From 15e426c0056a4f4b0b79d456ed4e378bbdf7f106 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Thu, 6 Feb 2025 19:31:23 -0500
Subject: [PATCH 01/34] Initial version detection

---
 .../modules/IDE/components/Header/Toolbar.jsx |   4 +
 .../modules/IDE/components/VersionPicker.jsx  |  12 ++
 client/modules/IDE/hooks/useP5Version.js      | 184 ++++++++++++++++++
 3 files changed, 200 insertions(+)
 create mode 100644 client/modules/IDE/components/VersionPicker.jsx
 create mode 100644 client/modules/IDE/hooks/useP5Version.js

diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx
index 5ebdbdafea..a15416011a 100644
--- a/client/modules/IDE/components/Header/Toolbar.jsx
+++ b/client/modules/IDE/components/Header/Toolbar.jsx
@@ -20,6 +20,7 @@ import PlayIcon from '../../../../images/play.svg';
 import StopIcon from '../../../../images/stop.svg';
 import PreferencesIcon from '../../../../images/preferences.svg';
 import ProjectName from './ProjectName';
+import VersionPicker from '../VersionPicker';
 
 const Toolbar = (props) => {
   const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector(
@@ -97,6 +98,9 @@ const Toolbar = (props) => {
           {t('Toolbar.Auto-refresh')}
         </label>
       </div>
+      <div className="toolbar__version-picker">
+        <VersionPicker />
+      </div>
       <div className="toolbar__project-name-container">
         <ProjectName />
         {(() => {
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
new file mode 100644
index 0000000000..158be4691f
--- /dev/null
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { useP5Version } from '../hooks/useP5Version';
+
+const VersionPicker = () => {
+  const versionInfo = useP5Version();
+
+  return <p>{versionInfo?.version || 'custom'}</p>;
+};
+
+VersionPicker.popTypes = {};
+
+export default VersionPicker;
diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
new file mode 100644
index 0000000000..2e345eace7
--- /dev/null
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -0,0 +1,184 @@
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+
+// Generated from https://www.npmjs.com/package/p5?activeTab=versions
+// Run this in the console:
+// JSON.stringify([...document.querySelectorAll('._132722c7')].map(n => n.innerText), null, 2)
+// TODO: use their API for this to grab these at build time?
+export const p5Versions = [
+  '1.11.3',
+  '2.0.0-beta.2',
+  '1.11.3',
+  '1.11.2',
+  '1.11.1',
+  '1.11.0',
+  '1.10.0',
+  '1.9.4',
+  '1.9.3',
+  '1.9.2',
+  '1.9.1',
+  '1.9.0',
+  '1.8.0',
+  '1.7.0',
+  '1.6.0',
+  '1.5.0',
+  '1.4.2',
+  '1.4.1',
+  '1.4.0',
+  '1.3.1',
+  '1.3.0',
+  '1.2.0',
+  '1.1.9',
+  '1.1.8',
+  '1.1.7',
+  '1.1.5',
+  '1.1.4',
+  '1.1.3',
+  '1.1.2',
+  '1.1.1',
+  '1.1.0',
+  '1.0.0',
+  '0.10.2',
+  '0.10.1',
+  '0.10.0',
+  '0.9.0',
+  '0.8.0',
+  '0.7.3',
+  '0.7.2',
+  '0.7.1',
+  '0.7.0',
+  '0.6.1',
+  '0.6.0',
+  '0.5.16',
+  '0.5.15',
+  '0.5.14',
+  '0.5.13',
+  '0.5.12',
+  '0.5.11',
+  '0.5.10',
+  '0.5.9',
+  '0.5.8',
+  '0.5.7',
+  '0.5.6',
+  '0.5.5',
+  '0.5.4',
+  '0.5.3',
+  '0.5.2',
+  '0.5.1',
+  '0.5.0',
+  '0.4.24',
+  '0.4.23',
+  '0.4.22',
+  '0.4.21',
+  '0.4.20',
+  '0.4.19',
+  '0.4.18',
+  '0.4.17',
+  '0.4.16',
+  '0.4.15',
+  '0.4.14',
+  '0.4.13',
+  '0.4.12',
+  '0.4.11',
+  '0.4.10',
+  '0.4.9',
+  '0.4.8',
+  '0.4.7',
+  '0.4.6',
+  '0.4.5',
+  '0.4.4',
+  '0.4.3',
+  '0.4.2',
+  '0.4.1',
+  '0.4.0',
+  '0.3.16',
+  '0.3.15',
+  '0.3.14',
+  '0.3.13',
+  '0.3.12',
+  '0.3.11',
+  '0.3.10',
+  '0.3.9',
+  '0.3.8',
+  '0.3.7',
+  '0.3.6',
+  '0.3.5',
+  '0.3.4',
+  '0.3.3',
+  '0.3.2',
+  '0.3.1',
+  '0.3.0',
+  '0.2.23',
+  '0.2.22',
+  '0.2.21',
+  '0.2.20',
+  '0.2.19',
+  '0.2.18',
+  '0.2.17',
+  '0.2.16',
+  '0.2.15',
+  '0.2.14',
+  '0.2.13',
+  '0.2.12',
+  '0.2.11',
+  '0.2.10',
+  '0.2.9',
+  '0.2.8',
+  '0.2.7',
+  '0.2.6',
+  '0.2.5',
+  '0.2.4',
+  '0.2.3',
+  '0.2.2',
+  '0.2.1'
+];
+
+export function useP5Version() {
+  const files = useSelector((state) => state.files);
+  const indexSrc = files.find(
+    (file) =>
+      file.fileType === 'file' &&
+      file.name === 'index.html' &&
+      file.filePath === ''
+  )?.content;
+
+  // { version: string, minified: boolean, replaceVersion: (version: string) => string } | null
+  const versionInfo = useMemo(() => {
+    if (!indexSrc) return null;
+    const dom = new DOMParser().parseFromString(indexSrc, 'text/html');
+    const usedP5Versions = [...dom.documentElement.querySelectorAll('script')]
+      .map((scriptNode) => {
+        const src = scriptNode.getAttribute('src') || '';
+        const matches = [
+          /^https?:\/\/cdnjs.cloudflare.com\/ajax\/libs\/p5.js\/(.+)\/p5\.(?:min\.)?js$/,
+          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5@(.+)\/lib\/p5\.(min\.)?js$/
+        ].map((regex) => regex.exec(src));
+        const match = matches.find((m) => m); // Find first that matched
+        if (!match) return null;
+
+        // See if this is a version we recognize
+        if (p5Versions.includes(match[1])) {
+          return { version: match[1], minified: !!match[2], scriptNode };
+        }
+        return null;
+      })
+      .filter((version) => version !== null);
+
+    // We only know for certain which one we've got if
+    if (usedP5Versions.length === 1) {
+      const { version, minified, scriptNode } = usedP5Versions[0];
+      const replaceVersion = function (newVersion) {
+        const file = minified ? 'p5.min.js' : 'p5.js';
+        scriptNode.setAttribute(
+          'src',
+          `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${newVersion}/${file}`
+        );
+        return dom.documentElement.outerHTML;
+      };
+      return { version, minified, replaceVersion };
+    }
+    return null;
+  }, [indexSrc]);
+
+  return versionInfo;
+}

From 5acd3ea415ebb137ed53f9d9f56c3675147e4ab0 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Thu, 6 Feb 2025 20:17:57 -0500
Subject: [PATCH 02/34] Half working version switcher

---
 .../modules/IDE/components/Editor/index.jsx   |  3 ++
 .../modules/IDE/components/VersionPicker.jsx  | 33 ++++++++++++++++---
 client/modules/IDE/hooks/useP5Version.js      |  9 ++---
 3 files changed, 37 insertions(+), 8 deletions(-)

diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index 8393116308..ef503a14c5 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -257,6 +257,9 @@ class Editor extends React.Component {
       if (!prevProps.unsavedChanges) {
         setTimeout(() => this.props.setUnsavedChanges(false), 400);
       }
+    } else if (this.getContent().content !== this.props.file.content) {
+      // TODO: make this not break regular edits!
+      this._cm.setValue(this.props.file.content);
     }
     if (this.props.fontSize !== prevProps.fontSize) {
       this._cm.getWrapperElement().style[
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 158be4691f..08ba45fe5b 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -1,10 +1,35 @@
-import React from 'react';
-import { useP5Version } from '../hooks/useP5Version';
+import React, { useCallback } from 'react';
+import { useDispatch } from 'react-redux';
+
+import { useP5Version, p5Versions } from '../hooks/useP5Version';
+import MenuItem from '../../../components/Dropdown/MenuItem';
+import DropdownMenu from '../../../components/Dropdown/DropdownMenu';
+import { updateFileContent } from '../actions/files';
 
 const VersionPicker = () => {
-  const versionInfo = useP5Version();
+  const { indexID, versionInfo } = useP5Version();
+  const dispatch = useDispatch();
+  const dispatchReplaceVersion = useCallback(
+    (version) => {
+      if (!indexID || !versionInfo) return;
+      dispatch(updateFileContent(indexID, versionInfo.replaceVersion(version)));
+    },
+    [indexID, versionInfo]
+  );
+
+  if (!versionInfo) {
+    return <p>Custom</p>;
+  }
 
-  return <p>{versionInfo?.version || 'custom'}</p>;
+  return (
+    <DropdownMenu anchor={<span>Version</span>} align="left">
+      {p5Versions.map((version) => (
+        <MenuItem key={version} onClick={() => dispatchReplaceVersion(version)}>
+          {version}
+        </MenuItem>
+      ))}
+    </DropdownMenu>
+  );
 };
 
 VersionPicker.popTypes = {};
diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
index 2e345eace7..f5ed3cc657 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -8,7 +8,6 @@ import { useSelector } from 'react-redux';
 export const p5Versions = [
   '1.11.3',
   '2.0.0-beta.2',
-  '1.11.3',
   '1.11.2',
   '1.11.1',
   '1.11.0',
@@ -135,12 +134,14 @@ export const p5Versions = [
 
 export function useP5Version() {
   const files = useSelector((state) => state.files);
-  const indexSrc = files.find(
+  const indexFile = files.find(
     (file) =>
       file.fileType === 'file' &&
       file.name === 'index.html' &&
       file.filePath === ''
-  )?.content;
+  );
+  const indexSrc = indexFile?.content;
+  const indexID = indexFile?.id;
 
   // { version: string, minified: boolean, replaceVersion: (version: string) => string } | null
   const versionInfo = useMemo(() => {
@@ -180,5 +181,5 @@ export function useP5Version() {
     return null;
   }, [indexSrc]);
 
-  return versionInfo;
+  return { indexID, versionInfo };
 }

From e0342fb0f0d0f3af648d6b59390a83476dd9ae4f Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Thu, 6 Feb 2025 20:25:09 -0500
Subject: [PATCH 03/34] Show version in picker

---
 client/modules/IDE/components/Editor/index.jsx  | 2 +-
 client/modules/IDE/components/VersionPicker.jsx | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index ef503a14c5..93c0007c9b 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -259,7 +259,7 @@ class Editor extends React.Component {
       }
     } else if (this.getContent().content !== this.props.file.content) {
       // TODO: make this not break regular edits!
-      this._cm.setValue(this.props.file.content);
+      // this._cm.setValue(this.props.file.content);
     }
     if (this.props.fontSize !== prevProps.fontSize) {
       this._cm.getWrapperElement().style[
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 08ba45fe5b..1291ec51e6 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -8,6 +8,7 @@ import { updateFileContent } from '../actions/files';
 
 const VersionPicker = () => {
   const { indexID, versionInfo } = useP5Version();
+  console.log(versionInfo);
   const dispatch = useDispatch();
   const dispatchReplaceVersion = useCallback(
     (version) => {
@@ -22,7 +23,7 @@ const VersionPicker = () => {
   }
 
   return (
-    <DropdownMenu anchor={<span>Version</span>} align="left">
+    <DropdownMenu anchor={<span>{versionInfo.version}</span>} align="left">
       {p5Versions.map((version) => (
         <MenuItem key={version} onClick={() => dispatchReplaceVersion(version)}>
           {version}

From cf963b701311ef2e09654db3a5baee112884cfcf Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 19 Feb 2025 19:09:51 -0500
Subject: [PATCH 04/34] Switch to controlled preferences tabs; create version
 button

---
 client/constants.js                           |  1 +
 client/modules/IDE/actions/preferences.js     |  7 +++++
 .../modules/IDE/components/Editor/index.jsx   |  4 +++
 .../IDE/components/Preferences/index.jsx      | 16 ++++++++--
 .../IDE/components/VersionIndicator.jsx       | 30 +++++++++++++++++++
 .../modules/IDE/components/VersionPicker.jsx  |  3 +-
 client/modules/IDE/reducers/preferences.js    |  5 ++++
 client/styles/components/_editor.scss         | 14 +++++++++
 translations/locales/en-US/translations.json  |  5 +++-
 9 files changed, 80 insertions(+), 5 deletions(-)
 create mode 100644 client/modules/IDE/components/VersionIndicator.jsx

diff --git a/client/constants.js b/client/constants.js
index fa6010aed1..57c85f4e85 100644
--- a/client/constants.js
+++ b/client/constants.js
@@ -10,6 +10,7 @@ export const STOP_ACCESSIBLE_OUTPUT = 'STOP_ACCESSIBLE_OUTPUT';
 
 export const OPEN_PREFERENCES = 'OPEN_PREFERENCES';
 export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
+export const SET_PREFERENCES_TAB = 'SET_PREFERENCES_TAB';
 export const SET_FONT_SIZE = 'SET_FONT_SIZE';
 export const SET_LINE_NUMBERS = 'SET_LINE_NUMBERS';
 
diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js
index e0473bd995..ebaefd1625 100644
--- a/client/modules/IDE/actions/preferences.js
+++ b/client/modules/IDE/actions/preferences.js
@@ -14,6 +14,13 @@ function updatePreferences(formParams, dispatch) {
     });
 }
 
+export function setPreferencesTab(value) {
+  return {
+    type: ActionTypes.SET_PREFERENCES_TAB,
+    value
+  };
+}
+
 export function setFontSize(value) {
   return (dispatch, getState) => {
     // eslint-disable-line
diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index 93c0007c9b..7e8f502583 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -72,6 +72,7 @@ import UnsavedChangesIndicator from '../UnsavedChangesIndicator';
 import { EditorContainer, EditorHolder } from './MobileEditor';
 import { FolderIcon } from '../../../../common/icons';
 import IconButton from '../../../../common/IconButton';
+import VersionIndicator from '../VersionIndicator';
 
 emmet(CodeMirror);
 
@@ -565,6 +566,9 @@ class Editor extends React.Component {
                   </span>
                   <Timer />
                 </div>
+                <div className="editor__library-version">
+                  <VersionIndicator />
+                </div>
               </div>
               <article
                 ref={(element) => {
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index fa5859400e..014ed3db3d 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -16,7 +16,8 @@ import {
   setLintWarning,
   setAutocloseBracketsQuotes,
   setAutocompleteHinter,
-  setLinewrap
+  setLinewrap,
+  setPreferencesTab
 } from '../../actions/preferences';
 
 export default function Preferences() {
@@ -25,6 +26,7 @@ export default function Preferences() {
   const dispatch = useDispatch();
 
   const {
+    tabIndex,
     fontSize,
     autosave,
     linewrap,
@@ -78,6 +80,10 @@ export default function Preferences() {
     handleFontSize(newValue);
   }
 
+  function changeTab(index) {
+    dispatch(setPreferencesTab(index));
+  }
+
   const fontSizeInputRef = useRef(null);
 
   return (
@@ -85,7 +91,7 @@ export default function Preferences() {
       <Helmet>
         <title>p5.js Web Editor | Preferences</title>
       </Helmet>
-      <Tabs>
+      <Tabs selectedIndex={tabIndex} onSelect={changeTab}>
         <TabList>
           <div className="tabs__titles">
             <Tab>
@@ -96,6 +102,11 @@ export default function Preferences() {
             <Tab>
               <h4 className="tabs__title">{t('Preferences.Accessibility')}</h4>
             </Tab>
+            <Tab>
+              <h4 className="tabs__title">
+                {t('Preferences.LibraryManagement')}
+              </h4>
+            </Tab>
           </div>
         </TabList>
         <TabPanel>
@@ -455,6 +466,7 @@ export default function Preferences() {
             </div>
           </div>
         </TabPanel>
+        <TabPanel>TODO</TabPanel>
       </Tabs>
     </section>
   );
diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
new file mode 100644
index 0000000000..317514022c
--- /dev/null
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -0,0 +1,30 @@
+import React, { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { openPreferences } from '../actions/ide';
+import { setPreferencesTab } from '../actions/preferences';
+
+import { useP5Version } from '../hooks/useP5Version';
+
+const VersionIndicator = () => {
+  const { versionInfo } = useP5Version();
+  const { t } = useTranslation();
+  const dispatch = useDispatch();
+
+  const openVersionSettings = useCallback(() => {
+    dispatch(openPreferences());
+    dispatch(setPreferencesTab(2));
+  }, []);
+
+  return (
+    <button onClick={openVersionSettings}>
+      {t('Toolbar.LibraryVersion')}
+      &nbsp;
+      {versionInfo?.version || t('Toolbar.CustomLibraryVersion')}
+    </button>
+  );
+};
+
+VersionIndicator.propTypes = {};
+
+export default VersionIndicator;
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 1291ec51e6..977735fde8 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -8,7 +8,6 @@ import { updateFileContent } from '../actions/files';
 
 const VersionPicker = () => {
   const { indexID, versionInfo } = useP5Version();
-  console.log(versionInfo);
   const dispatch = useDispatch();
   const dispatchReplaceVersion = useCallback(
     (version) => {
@@ -33,6 +32,6 @@ const VersionPicker = () => {
   );
 };
 
-VersionPicker.popTypes = {};
+VersionPicker.propTypes = {};
 
 export default VersionPicker;
diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js
index 087d927aed..630fa465ef 100644
--- a/client/modules/IDE/reducers/preferences.js
+++ b/client/modules/IDE/reducers/preferences.js
@@ -1,6 +1,7 @@
 import * as ActionTypes from '../../../constants';
 
 export const initialState = {
+  tabIndex: 0,
   fontSize: 18,
   autosave: true,
   linewrap: true,
@@ -17,6 +18,10 @@ export const initialState = {
 
 const preferences = (state = initialState, action) => {
   switch (action.type) {
+    case ActionTypes.OPEN_PREFERENCES:
+      return Object.assign({}, state, { tabIndex: 0 });
+    case ActionTypes.SET_PREFERENCES_TAB:
+      return Object.assign({}, state, { tabIndex: action.value });
     case ActionTypes.SET_FONT_SIZE:
       return Object.assign({}, state, { fontSize: action.value });
     case ActionTypes.SET_AUTOSAVE:
diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss
index 0b92dec37b..0aab1d4b9f 100644
--- a/client/styles/components/_editor.scss
+++ b/client/styles/components/_editor.scss
@@ -398,11 +398,25 @@ pre.CodeMirror-line {
   height: #{math.div(29, $base-font-size)}rem;
   padding-top: #{math.div(7, $base-font-size)}rem;
   padding-left: #{math.div(56, $base-font-size)}rem;
+  padding-right: #{math.div(168, $base-font-size)}rem;
   font-size: #{math.div(12, $base-font-size)}rem;
   display: flex;
   justify-content: space-between;
 }
 
+.editor__library-version {
+  @include themify() {
+    color: getThemifyVariable("primary-text-color");
+  }
+  position: absolute;
+  top: 0;
+  right: 0;
+  display: flex;
+  justify-content: flex-end;
+  height: #{math.div(29, $base-font-size)}rem;
+  width: #{math.div(168, $base-font-size)}rem;
+}
+
 .editor__unsaved-changes {
   margin-left: #{math.div(2, $base-font-size)}rem;
 }
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 697e5a2749..312240627f 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -153,7 +153,9 @@
     "StopSketchARIA": "Stop sketch",
     "EditSketchARIA": "Edit sketch name",
     "NewSketchNameARIA": "New sketch name",
-    "By": " by "
+    "By": " by ",
+    "LibraryVersion": "p5.js Version:",
+    "CustomLibraryVersion": "Custom"
   },
   "Console": {
     "Title": "Console",
@@ -168,6 +170,7 @@
     "Settings": "Settings",
     "GeneralSettings": "General settings",
     "Accessibility": "Accessibility",
+    "LibraryManagement": "Library Management",
     "Theme": "Theme",
     "LightTheme": "Light",
     "LightThemeARIA": "light theme on",

From 5042faa46caed21dede3d61f9e25eb6c49514a0b Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 19 Feb 2025 19:42:32 -0500
Subject: [PATCH 05/34] Add info for custom versions

---
 .../IDE/components/Preferences/index.jsx      | 45 ++++++++++++++++++-
 .../modules/IDE/components/VersionPicker.jsx  | 13 +++++-
 client/styles/components/_preferences.scss    |  9 ++++
 translations/locales/en-US/translations.json  |  6 ++-
 4 files changed, 69 insertions(+), 4 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 014ed3db3d..9d92b1ae8b 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -19,6 +19,8 @@ import {
   setLinewrap,
   setPreferencesTab
 } from '../../actions/preferences';
+import { useP5Version } from '../../hooks/useP5Version';
+import VersionPicker from '../VersionPicker';
 
 export default function Preferences() {
   const { t } = useTranslation();
@@ -40,6 +42,7 @@ export default function Preferences() {
   } = useSelector((state) => state.preferences);
 
   const [state, setState] = useState({ fontSize });
+  const { versionInfo } = useP5Version();
 
   function onFontInputChange(event) {
     const INTEGER_REGEX = /^[0-9\b]+$/;
@@ -466,7 +469,47 @@ export default function Preferences() {
             </div>
           </div>
         </TabPanel>
-        <TabPanel>TODO</TabPanel>
+        <TabPanel>
+          <div className="preference">
+            <h4 className="preference__title">
+              {t('Preferences.LibraryVersion')}
+            </h4>
+            <div className="preference__options">
+              <VersionPicker />
+            </div>
+            <div className="preference__subtitle">
+              {versionInfo ? (
+                <p className="preference__paragraph">
+                  {t('Preferences.LibraryVersionInfo')}
+                </p>
+              ) : (
+                <>
+                  <p className="preference__paragraph">
+                    {t('Preferences.CustomVersionInfo')}
+                  </p>
+                  <p className="preference__paragraph">
+                    {t('Preferences.CustomVersionReset')}
+                  </p>
+                  <textarea className="preference__textarea">
+                    {'<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>\n' +
+                      '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js"></script>'}
+                  </textarea>
+                </>
+              )}
+            </div>
+          </div>
+          {versionInfo ? (
+            <>
+              <p>Test</p>
+              <p>Test</p>
+            </>
+          ) : (
+            <>
+              <p>Test</p>
+              <p>Test</p>
+            </>
+          )}
+        </TabPanel>
       </Tabs>
     </section>
   );
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 977735fde8..36becc7a4d 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 import { useDispatch } from 'react-redux';
+import { useTranslation } from 'react-i18next';
 
 import { useP5Version, p5Versions } from '../hooks/useP5Version';
 import MenuItem from '../../../components/Dropdown/MenuItem';
@@ -8,6 +9,7 @@ import { updateFileContent } from '../actions/files';
 
 const VersionPicker = () => {
   const { indexID, versionInfo } = useP5Version();
+  const { t } = useTranslation();
   const dispatch = useDispatch();
   const dispatchReplaceVersion = useCallback(
     (version) => {
@@ -18,11 +20,18 @@ const VersionPicker = () => {
   );
 
   if (!versionInfo) {
-    return <p>Custom</p>;
+    return (
+      <button disabled className="versionPicker">
+        {t('Toolbar.CustomLibraryVersion')}
+      </button>
+    );
   }
 
   return (
-    <DropdownMenu anchor={<span>{versionInfo.version}</span>} align="left">
+    <DropdownMenu
+      anchor={<button className="versionPicker">{versionInfo.version}</button>}
+      align="left"
+    >
       {p5Versions.map((version) => (
         <MenuItem key={version} onClick={() => dispatchReplaceVersion(version)}>
           {version}
diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss
index e0c868041a..277f70fce4 100644
--- a/client/styles/components/_preferences.scss
+++ b/client/styles/components/_preferences.scss
@@ -67,6 +67,15 @@
   margin-top: 0;
 }
 
+.preference__paragraph {
+  margin-bottom: #{math.div(10, $base-font-size)}rem;
+}
+
+.preference__textarea {
+  width: 100%;
+  min-height: 8em;
+}
+
 .preference__value {
   @include themify() {
     border: #{math.div(1, $base-font-size)}rem solid
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 312240627f..5d8c9f1e75 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -212,7 +212,11 @@
     "PlainText": "Plain-text",
     "TextOutputARIA": "text output on",
     "TableText": "Table-text",
-    "TableOutputARIA": "table output on"
+    "TableOutputARIA": "table output on",
+    "LibraryVersion": "p5.js Version",
+    "LibraryVersionInfo": "TODO Add helpful info about the new p5.js version, compatibility, etc.",
+    "CustomVersionInfo": "It looks like you've changed the <script> tag to manage the version yourself. In that case, the version can't be managed from this tab, only from the code in index.html.",
+    "CustomVersionReset": "If you do want to use the default libraries, you can replace your script tags in index.html to the following:"
   },
   "KeyboardShortcuts": {
     "Title": " Keyboard Shortcuts",

From 066bdf69682a61aa59f2a03f6abb18eece025c3d Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 19 Feb 2025 19:55:06 -0500
Subject: [PATCH 06/34] Add placeholders for other settings

---
 .../IDE/components/Preferences/index.jsx      | 127 ++++++++++++++++--
 translations/locales/en-US/translations.json  |   5 +-
 2 files changed, 120 insertions(+), 12 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 9d92b1ae8b..577aa93d17 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -474,10 +474,8 @@ export default function Preferences() {
             <h4 className="preference__title">
               {t('Preferences.LibraryVersion')}
             </h4>
-            <div className="preference__options">
+            <div>
               <VersionPicker />
-            </div>
-            <div className="preference__subtitle">
               {versionInfo ? (
                 <p className="preference__paragraph">
                   {t('Preferences.LibraryVersionInfo')}
@@ -498,15 +496,122 @@ export default function Preferences() {
               )}
             </div>
           </div>
-          {versionInfo ? (
-            <>
-              <p>Test</p>
-              <p>Test</p>
-            </>
-          ) : (
+          {versionInfo && (
             <>
-              <p>Test</p>
-              <p>Test</p>
+              <div className="preference">
+                <h4 className="preference__title">
+                  {t('Preferences.SoundAddon')}
+                </h4>
+                <div className="preference__options">
+                  <input
+                    type="radio"
+                    onChange={() => dispatch(setAutosave(true))}
+                    aria-label={t('Preferences.AutosaveOnARIA')}
+                    name="soundaddon"
+                    id="soundaddon-on"
+                    className="preference__radio-button"
+                    value="On"
+                    checked={autosave}
+                  />
+                  <label htmlFor="soundaddon-on" className="preference__option">
+                    {t('Preferences.On')}
+                  </label>
+                  <input
+                    type="radio"
+                    onChange={() => dispatch(setAutosave(false))}
+                    aria-label={t('Preferences.AutosaveOffARIA')}
+                    name="soundaddon"
+                    id="soundaddon-off"
+                    className="preference__radio-button"
+                    value="Off"
+                    checked={!autosave}
+                  />
+                  <label
+                    htmlFor="soundaddon-off"
+                    className="preference__option"
+                  >
+                    {t('Preferences.Off')}
+                  </label>
+                </div>
+              </div>
+              <div className="preference">
+                <h4 className="preference__title">
+                  {t('Preferences.PreloadAddon')}
+                </h4>
+                <div className="preference__options">
+                  <input
+                    type="radio"
+                    onChange={() => dispatch(setAutosave(true))}
+                    aria-label={t('Preferences.AutosaveOnARIA')}
+                    name="preloadaddon"
+                    id="preloadaddon-on"
+                    className="preference__radio-button"
+                    value="On"
+                    checked={autosave}
+                  />
+                  <label
+                    htmlFor="preloadaddon-on"
+                    className="preference__option"
+                  >
+                    {t('Preferences.On')}
+                  </label>
+                  <input
+                    type="radio"
+                    onChange={() => dispatch(setAutosave(false))}
+                    aria-label={t('Preferences.AutosaveOffARIA')}
+                    name="preloadaddon"
+                    id="preloadaddon-off"
+                    className="preference__radio-button"
+                    value="Off"
+                    checked={!autosave}
+                  />
+                  <label
+                    htmlFor="preloadaddon-off"
+                    className="preference__option"
+                  >
+                    {t('Preferences.Off')}
+                  </label>
+                </div>
+              </div>
+              <div className="preference">
+                <h4 className="preference__title">
+                  {t('Preferences.ShapesAddon')}
+                </h4>
+                <div className="preference__options">
+                  <input
+                    type="radio"
+                    onChange={() => dispatch(setAutosave(true))}
+                    aria-label={t('Preferences.AutosaveOnARIA')}
+                    name="shapesaddon"
+                    id="shapesaddon-on"
+                    className="preference__radio-button"
+                    value="On"
+                    checked={autosave}
+                  />
+                  <label
+                    htmlFor="shapesaddon-on"
+                    className="preference__option"
+                  >
+                    {t('Preferences.On')}
+                  </label>
+                  <input
+                    type="radio"
+                    onChange={() => dispatch(setAutosave(false))}
+                    aria-label={t('Preferences.AutosaveOffARIA')}
+                    name="shapesaddon"
+                    id="shapesaddon-off"
+                    className="preference__radio-button"
+                    value="Off"
+                    checked={!autosave}
+                  />
+                  <label
+                    htmlFor="shapesaddon-off"
+                    className="preference__option"
+                  >
+                    {t('Preferences.Off')}
+                  </label>
+                </div>
+              </div>
             </>
           )}
         </TabPanel>
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 5d8c9f1e75..adcf17f461 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -216,7 +216,10 @@
     "LibraryVersion": "p5.js Version",
     "LibraryVersionInfo": "TODO Add helpful info about the new p5.js version, compatibility, etc.",
     "CustomVersionInfo": "It looks like you've changed the <script> tag to manage the version yourself. In that case, the version can't be managed from this tab, only from the code in index.html.",
-    "CustomVersionReset": "If you do want to use the default libraries, you can replace your script tags in index.html to the following:"
+    "CustomVersionReset": "If you do want to use the default libraries, you can replace your script tags in index.html to the following:",
+    "SoundAddon": "p5.sound.js Addon",
+    "PreloadAddon": "p5.js 2.0 Addon - Preload",
+    "ShapesAddon": "p5.js 2.0 Addon - Shapes"
   },
   "KeyboardShortcuts": {
     "Title": " Keyboard Shortcuts",

From 89f0d5e141d48355718a31a6ffd65232ea2776e0 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 19 Feb 2025 20:17:57 -0500
Subject: [PATCH 07/34] Start to add code to do addon toggles

---
 .../IDE/components/Preferences/index.jsx      | 70 +++++++++++++++----
 .../modules/IDE/components/VersionPicker.jsx  |  6 +-
 client/modules/IDE/hooks/useP5Version.js      | 57 ++++++++++++++-
 3 files changed, 113 insertions(+), 20 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 577aa93d17..15a1c78bd5 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -21,6 +21,7 @@ import {
 } from '../../actions/preferences';
 import { useP5Version } from '../../hooks/useP5Version';
 import VersionPicker from '../VersionPicker';
+import { updateFileContent } from '../../actions/files';
 
 export default function Preferences() {
   const { t } = useTranslation();
@@ -42,7 +43,7 @@ export default function Preferences() {
   } = useSelector((state) => state.preferences);
 
   const [state, setState] = useState({ fontSize });
-  const { versionInfo } = useP5Version();
+  const { versionInfo, indexID } = useP5Version();
 
   function onFontInputChange(event) {
     const INTEGER_REGEX = /^[0-9\b]+$/;
@@ -476,7 +477,7 @@ export default function Preferences() {
             </h4>
             <div>
               <VersionPicker />
-              {versionInfo ? (
+              {versionInfo && indexID ? (
                 <p className="preference__paragraph">
                   {t('Preferences.LibraryVersionInfo')}
                 </p>
@@ -496,7 +497,7 @@ export default function Preferences() {
               )}
             </div>
           </div>
-          {versionInfo && (
+          {versionInfo && indexID && (
             <>
               <div className="preference">
                 <h4 className="preference__title">
@@ -505,26 +506,37 @@ export default function Preferences() {
                 <div className="preference__options">
                   <input
                     type="radio"
-                    onChange={() => dispatch(setAutosave(true))}
+                    onChange={() =>
+                      dispatch(
+                        updateFileContent(indexID, versionInfo.setP5Sound(true))
+                      )
+                    }
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="soundaddon"
                     id="soundaddon-on"
                     className="preference__radio-button"
                     value="On"
-                    checked={autosave}
+                    checked={versionInfo.p5Sound}
                   />
                   <label htmlFor="soundaddon-on" className="preference__option">
                     {t('Preferences.On')}
                   </label>
                   <input
                     type="radio"
-                    onChange={() => dispatch(setAutosave(false))}
+                    onChange={() =>
+                      dispatch(
+                        updateFileContent(
+                          indexID,
+                          versionInfo.setP5Sound(false)
+                        )
+                      )
+                    }
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="soundaddon"
                     id="soundaddon-off"
                     className="preference__radio-button"
                     value="Off"
-                    checked={!autosave}
+                    checked={!versionInfo.p5Sound}
                   />
                   <label
                     htmlFor="soundaddon-off"
@@ -541,13 +553,20 @@ export default function Preferences() {
                 <div className="preference__options">
                   <input
                     type="radio"
-                    onChange={() => dispatch(setAutosave(true))}
+                    onChange={() =>
+                      dispatch(
+                        updateFileContent(
+                          indexID,
+                          versionInfo.setP5PreloadAddon(true)
+                        )
+                      )
+                    }
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="preloadaddon"
                     id="preloadaddon-on"
                     className="preference__radio-button"
                     value="On"
-                    checked={autosave}
+                    checked={versionInfo.p5PreloadAddon}
                   />
                   <label
                     htmlFor="preloadaddon-on"
@@ -557,13 +576,20 @@ export default function Preferences() {
                   </label>
                   <input
                     type="radio"
-                    onChange={() => dispatch(setAutosave(false))}
+                    onChange={() =>
+                      dispatch(
+                        updateFileContent(
+                          indexID,
+                          versionInfo.setP5PreloadAddon(false)
+                        )
+                      )
+                    }
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="preloadaddon"
                     id="preloadaddon-off"
                     className="preference__radio-button"
                     value="Off"
-                    checked={!autosave}
+                    checked={!versionInfo.p5PreloadAddon}
                   />
                   <label
                     htmlFor="preloadaddon-off"
@@ -580,13 +606,20 @@ export default function Preferences() {
                 <div className="preference__options">
                   <input
                     type="radio"
-                    onChange={() => dispatch(setAutosave(true))}
+                    onChange={() =>
+                      dispatch(
+                        updateFileContent(
+                          indexID,
+                          versionInfo.setP5ShapesAddon(true)
+                        )
+                      )
+                    }
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="shapesaddon"
                     id="shapesaddon-on"
                     className="preference__radio-button"
                     value="On"
-                    checked={autosave}
+                    checked={versionInfo.p5ShapesAdddon}
                   />
                   <label
                     htmlFor="shapesaddon-on"
@@ -596,13 +629,20 @@ export default function Preferences() {
                   </label>
                   <input
                     type="radio"
-                    onChange={() => dispatch(setAutosave(false))}
+                    onChange={() =>
+                      dispatch(
+                        updateFileContent(
+                          indexID,
+                          versionInfo.setP5ShapesAddon(false)
+                        )
+                      )
+                    }
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="shapesaddon"
                     id="shapesaddon-off"
                     className="preference__radio-button"
                     value="Off"
-                    checked={!autosave}
+                    checked={!versionInfo.p5ShapesAdddon}
                   />
                   <label
                     htmlFor="shapesaddon-off"
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 36becc7a4d..3eb10b9e4b 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -21,15 +21,13 @@ const VersionPicker = () => {
 
   if (!versionInfo) {
     return (
-      <button disabled className="versionPicker">
-        {t('Toolbar.CustomLibraryVersion')}
-      </button>
+      <span className="versionPicker">{t('Toolbar.CustomLibraryVersion')}</span>
     );
   }
 
   return (
     <DropdownMenu
-      anchor={<button className="versionPicker">{versionInfo.version}</button>}
+      anchor={<span className="versionPicker">{versionInfo.version}</span>}
       align="left"
     >
       {p5Versions.map((version) => (
diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
index f5ed3cc657..5d753976db 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -132,6 +132,11 @@ export const p5Versions = [
   '0.2.1'
 ];
 
+export const p5SoundURL =
+  'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js';
+export const p5PreloadAddonURL = 'https://TODO/preload.js';
+export const p5ShapesAddonURL = 'https://TODO/shapes.js';
+
 export function useP5Version() {
   const files = useSelector((state) => state.files);
   const indexFile = files.find(
@@ -176,7 +181,57 @@ export function useP5Version() {
         );
         return dom.documentElement.outerHTML;
       };
-      return { version, minified, replaceVersion };
+
+      const p5SoundNode = [
+        ...dom.documentElement.querySelectorAll('script')
+      ].find((s) => s.getAttribute('src') === p5SoundURL);
+      const setP5Sound = function (enabled) {
+        if (!enabled && p5SoundNode) {
+          p5SoundNode.parentNode.removeChild(p5SoundNode);
+        } else if (enabled && !p5SoundNode) {
+          const newNode = document.createElement('script');
+          newNode.setAttribute('src', p5SoundURL);
+          scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
+        }
+      };
+
+      const p5PreloadAddonNode = [
+        ...dom.documentElement.querySelectorAll('script')
+      ].find((s) => s.getAttribute('src') === p5PreloadAddonURL);
+      const setP5PreloadAddon = function (enabled) {
+        if (!enabled && p5PreloadAddonNode) {
+          p5PreloadAddonNode.parentNode.removeChild(p5PreloadAddonNode);
+        } else if (enabled && !p5PreloadAddonNode) {
+          const newNode = document.createElement('script');
+          newNode.setAttribute('src', p5PreloadAddonURL);
+          scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
+        }
+      };
+
+      const p5ShapesAddonNode = [
+        ...dom.documentElement.querySelectorAll('script')
+      ].find((s) => s.getAttribute('src') === p5ShapesAddonURL);
+      const setP5ShapesAddon = function (enabled) {
+        if (!enabled && p5ShapesAddonNode) {
+          p5ShapesAddonNode.parentNode.removeChild(p5ShapesAddonNode);
+        } else if (enabled && !p5ShapesAddonNode) {
+          const newNode = document.createElement('script');
+          newNode.setAttribute('src', p5ShapesAddonURL);
+          scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
+        }
+      };
+
+      return {
+        version,
+        minified,
+        replaceVersion,
+        p5Sound: !!p5SoundNode,
+        setP5Sound,
+        p5PreloadAddon: !!p5PreloadAddonNode,
+        setP5PreloadAddon,
+        p5ShapesAdddon: !!p5ShapesAddonNode,
+        setP5ShapesAddon
+      };
     }
     return null;
   }, [indexSrc]);

From 6bd28e99795e8354fca7b4c33fd54a6fba9ede04 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 19 Feb 2025 20:24:13 -0500
Subject: [PATCH 08/34] Fix return values

---
 client/modules/IDE/hooks/useP5Version.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
index 5d753976db..94caa225c0 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -193,6 +193,7 @@ export function useP5Version() {
           newNode.setAttribute('src', p5SoundURL);
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
+        return dom.documentElement.outerHTML;
       };
 
       const p5PreloadAddonNode = [
@@ -206,6 +207,7 @@ export function useP5Version() {
           newNode.setAttribute('src', p5PreloadAddonURL);
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
+        return dom.documentElement.outerHTML;
       };
 
       const p5ShapesAddonNode = [
@@ -219,6 +221,7 @@ export function useP5Version() {
           newNode.setAttribute('src', p5ShapesAddonURL);
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
+        return dom.documentElement.outerHTML;
       };
 
       return {

From 1eac470850779239762c1ad52b93ce379e7c4f40 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 26 Feb 2025 19:02:39 -0500
Subject: [PATCH 09/34] Add doctype

---
 client/modules/IDE/hooks/useP5Version.js | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
index 94caa225c0..0da5680631 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -152,6 +152,19 @@ export function useP5Version() {
   const versionInfo = useMemo(() => {
     if (!indexSrc) return null;
     const dom = new DOMParser().parseFromString(indexSrc, 'text/html');
+
+    const serializeResult = () => {
+      let src = dom.documentElement.outerHTML;
+
+      if (dom.doctype) {
+        const doctype = new XMLSerializer().serializeToString(dom.doctype);
+        src = `${doctype}\n${src}`;
+      }
+
+      console.log(src);
+      return src;
+    };
+
     const usedP5Versions = [...dom.documentElement.querySelectorAll('script')]
       .map((scriptNode) => {
         const src = scriptNode.getAttribute('src') || '';
@@ -179,7 +192,7 @@ export function useP5Version() {
           'src',
           `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${newVersion}/${file}`
         );
-        return dom.documentElement.outerHTML;
+        return serializeResult();
       };
 
       const p5SoundNode = [
@@ -193,7 +206,7 @@ export function useP5Version() {
           newNode.setAttribute('src', p5SoundURL);
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
-        return dom.documentElement.outerHTML;
+        return serializeResult();
       };
 
       const p5PreloadAddonNode = [
@@ -207,7 +220,7 @@ export function useP5Version() {
           newNode.setAttribute('src', p5PreloadAddonURL);
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
-        return dom.documentElement.outerHTML;
+        return serializeResult();
       };
 
       const p5ShapesAddonNode = [
@@ -221,7 +234,7 @@ export function useP5Version() {
           newNode.setAttribute('src', p5ShapesAddonURL);
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
-        return dom.documentElement.outerHTML;
+        return serializeResult();
       };
 
       return {

From de95b134f216c883296332bf86b7071238cd9c91 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 26 Feb 2025 19:49:14 -0500
Subject: [PATCH 10/34] Make toggles actually do something!

---
 .../modules/IDE/components/Editor/index.jsx   | 18 ++++++-
 .../IDE/components/Preferences/index.jsx      | 52 +++++--------------
 .../modules/IDE/components/VersionPicker.jsx  | 10 ++--
 client/modules/IDE/hooks/useP5Version.js      |  1 -
 client/modules/IDE/pages/IDEView.jsx          |  4 +-
 5 files changed, 40 insertions(+), 45 deletions(-)

diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index 7e8f502583..677a9fd5b7 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -105,6 +105,7 @@ class Editor extends React.Component {
     this.showFind = this.showFind.bind(this);
     this.showReplace = this.showReplace.bind(this);
     this.getContent = this.getContent.bind(this);
+    this.updateFileContent = this.updateFileContent.bind(this);
   }
 
   componentDidMount() {
@@ -224,7 +225,8 @@ class Editor extends React.Component {
       tidyCode: this.tidyCode,
       showFind: this.showFind,
       showReplace: this.showReplace,
-      getContent: this.getContent
+      getContent: this.getContent,
+      updateFileContent: this.updateFileContent
     });
   }
 
@@ -336,7 +338,8 @@ class Editor extends React.Component {
       tidyCode: this.tidyCode,
       showFind: this.showFind,
       showReplace: this.showReplace,
-      getContent: this.getContent
+      getContent: this.getContent,
+      updateFileContent: this.updateFileContent
     });
   }
 
@@ -373,6 +376,17 @@ class Editor extends React.Component {
     return updatedFile;
   }
 
+  updateFileContent(id, src) {
+    const file = this._docs[id];
+    if (file) {
+      console.log(this._docs[id]);
+      this._docs[id] = CodeMirror.Doc(src, this._docs[id].modeOption);
+      if (id === this.props.file.id) {
+        this._cm.swapDoc(this._docs[id]);
+      }
+    }
+  }
+
   handleKeyUp = () => {
     const lineNumber = parseInt(this._cm.getCursor().line + 1, 10);
     this.setState({ currentLine: lineNumber });
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 15a1c78bd5..64f69f22ab 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -1,4 +1,4 @@
-import React, { useRef, useState } from 'react';
+import React, { useContext, useRef, useState } from 'react';
 import { Helmet } from 'react-helmet';
 import { useDispatch, useSelector } from 'react-redux';
 import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
@@ -22,6 +22,7 @@ import {
 import { useP5Version } from '../../hooks/useP5Version';
 import VersionPicker from '../VersionPicker';
 import { updateFileContent } from '../../actions/files';
+import { CmControllerContext } from '../../pages/IDEView';
 
 export default function Preferences() {
   const { t } = useTranslation();
@@ -44,6 +45,7 @@ export default function Preferences() {
 
   const [state, setState] = useState({ fontSize });
   const { versionInfo, indexID } = useP5Version();
+  const cmRef = useContext(CmControllerContext);
 
   function onFontInputChange(event) {
     const INTEGER_REGEX = /^[0-9\b]+$/;
@@ -90,6 +92,11 @@ export default function Preferences() {
 
   const fontSizeInputRef = useRef(null);
 
+  const updateHTML = (src) => {
+    dispatch(updateFileContent(indexID, src));
+    cmRef.current?.updateFileContent(indexID, src);
+  };
+
   return (
     <section className="preferences">
       <Helmet>
@@ -506,11 +513,7 @@ export default function Preferences() {
                 <div className="preference__options">
                   <input
                     type="radio"
-                    onChange={() =>
-                      dispatch(
-                        updateFileContent(indexID, versionInfo.setP5Sound(true))
-                      )
-                    }
+                    onChange={() => updateHTML(versionInfo.setP5Sound(true))}
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="soundaddon"
                     id="soundaddon-on"
@@ -523,14 +526,7 @@ export default function Preferences() {
                   </label>
                   <input
                     type="radio"
-                    onChange={() =>
-                      dispatch(
-                        updateFileContent(
-                          indexID,
-                          versionInfo.setP5Sound(false)
-                        )
-                      )
-                    }
+                    onChange={() => updateHTML(versionInfo.setP5Sound(false))}
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="soundaddon"
                     id="soundaddon-off"
@@ -554,12 +550,7 @@ export default function Preferences() {
                   <input
                     type="radio"
                     onChange={() =>
-                      dispatch(
-                        updateFileContent(
-                          indexID,
-                          versionInfo.setP5PreloadAddon(true)
-                        )
-                      )
+                      updateHTML(versionInfo.setP5PreloadAddon(true))
                     }
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="preloadaddon"
@@ -577,12 +568,7 @@ export default function Preferences() {
                   <input
                     type="radio"
                     onChange={() =>
-                      dispatch(
-                        updateFileContent(
-                          indexID,
-                          versionInfo.setP5PreloadAddon(false)
-                        )
-                      )
+                      updateHTML(versionInfo.setP5PreloadAddon(false))
                     }
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="preloadaddon"
@@ -607,12 +593,7 @@ export default function Preferences() {
                   <input
                     type="radio"
                     onChange={() =>
-                      dispatch(
-                        updateFileContent(
-                          indexID,
-                          versionInfo.setP5ShapesAddon(true)
-                        )
-                      )
+                      updateHTML(versionInfo.setP5ShapesAddon(true))
                     }
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="shapesaddon"
@@ -630,12 +611,7 @@ export default function Preferences() {
                   <input
                     type="radio"
                     onChange={() =>
-                      dispatch(
-                        updateFileContent(
-                          indexID,
-                          versionInfo.setP5ShapesAddon(false)
-                        )
-                      )
+                      updateHTML(versionInfo.setP5ShapesAddon(false))
                     }
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="shapesaddon"
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 3eb10b9e4b..331734436f 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useContext } from 'react';
 import { useDispatch } from 'react-redux';
 import { useTranslation } from 'react-i18next';
 
@@ -6,17 +6,21 @@ import { useP5Version, p5Versions } from '../hooks/useP5Version';
 import MenuItem from '../../../components/Dropdown/MenuItem';
 import DropdownMenu from '../../../components/Dropdown/DropdownMenu';
 import { updateFileContent } from '../actions/files';
+import { CmControllerContext } from '../pages/IDEView';
 
 const VersionPicker = () => {
   const { indexID, versionInfo } = useP5Version();
   const { t } = useTranslation();
   const dispatch = useDispatch();
+  const cmRef = useContext(CmControllerContext);
   const dispatchReplaceVersion = useCallback(
     (version) => {
       if (!indexID || !versionInfo) return;
-      dispatch(updateFileContent(indexID, versionInfo.replaceVersion(version)));
+      const src = versionInfo.replaceVersion(version);
+      dispatch(updateFileContent(indexID, src));
+      cmRef.current?.updateFileContent(indexID, src);
     },
-    [indexID, versionInfo]
+    [indexID, versionInfo, cmRef]
   );
 
   if (!versionInfo) {
diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
index 0da5680631..ac1b75b414 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -161,7 +161,6 @@ export function useP5Version() {
         src = `${doctype}\n${src}`;
       }
 
-      console.log(src);
       return src;
     };
 
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index 4084facb77..2c6ed4a496 100644
--- a/client/modules/IDE/pages/IDEView.jsx
+++ b/client/modules/IDE/pages/IDEView.jsx
@@ -281,7 +281,9 @@ const IDEView = () => {
           </SplitPane>
         </main>
       )}
-      <IDEOverlays />
+      <CmControllerContext.Provider value={cmRef}>
+        <IDEOverlays />
+      </CmControllerContext.Provider>
     </RootPage>
   );
 };

From 7b4ae4ec80e6e4469546c33499433b76a0a9c06b Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Thu, 27 Feb 2025 18:08:03 -0500
Subject: [PATCH 11/34] Add button to revert p5.sound version after a change

---
 .../modules/IDE/components/Editor/index.jsx   |  1 -
 .../IDE/components/Preferences/index.jsx      | 24 ++++++++++++++++--
 client/modules/IDE/hooks/useP5Version.js      | 25 ++++++++++++++++---
 client/styles/components/_preferences.scss    |  1 +
 translations/locales/en-US/translations.json  |  3 ++-
 5 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index 677a9fd5b7..5db295c8cd 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -379,7 +379,6 @@ class Editor extends React.Component {
   updateFileContent(id, src) {
     const file = this._docs[id];
     if (file) {
-      console.log(this._docs[id]);
       this._docs[id] = CodeMirror.Doc(src, this._docs[id].modeOption);
       if (id === this.props.file.id) {
         this._cm.swapDoc(this._docs[id]);
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 64f69f22ab..ea4c0b289a 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -19,10 +19,11 @@ import {
   setLinewrap,
   setPreferencesTab
 } from '../../actions/preferences';
-import { useP5Version } from '../../hooks/useP5Version';
+import { p5SoundURL, useP5Version } from '../../hooks/useP5Version';
 import VersionPicker from '../VersionPicker';
 import { updateFileContent } from '../../actions/files';
 import { CmControllerContext } from '../../pages/IDEView';
+import Button from '../../../../common/Button';
 
 export default function Preferences() {
   const { t } = useTranslation();
@@ -45,6 +46,7 @@ export default function Preferences() {
 
   const [state, setState] = useState({ fontSize });
   const { versionInfo, indexID } = useP5Version();
+  const [lastP5SoundURL, setLastP5SoundURL] = useState(undefined);
   const cmRef = useContext(CmControllerContext);
 
   function onFontInputChange(event) {
@@ -526,7 +528,15 @@ export default function Preferences() {
                   </label>
                   <input
                     type="radio"
-                    onChange={() => updateHTML(versionInfo.setP5Sound(false))}
+                    onChange={() => {
+                      // If the previous p5.sound.js script tag is not the
+                      // default version that one will get via this toggle,
+                      // record it so we can give the option to put it back
+                      if (versionInfo.p5SoundURL !== p5SoundURL) {
+                        setLastP5SoundURL(versionInfo.p5SoundURL);
+                      }
+                      updateHTML(versionInfo.setP5Sound(false));
+                    }}
                     aria-label={t('Preferences.AutosaveOffARIA')}
                     name="soundaddon"
                     id="soundaddon-off"
@@ -540,6 +550,16 @@ export default function Preferences() {
                   >
                     {t('Preferences.Off')}
                   </label>
+                  {lastP5SoundURL && (
+                    <Button
+                      onClick={() => {
+                        updateHTML(versionInfo.setP5SoundURL(lastP5SoundURL));
+                        setLastP5SoundURL(undefined);
+                      }}
+                    >
+                      {t('Preferences.UndoSoundVersion')}
+                    </Button>
+                  )}
                 </div>
               </div>
               <div className="preference">
diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.js
index ac1b75b414..bfabebdfc2 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.js
@@ -132,8 +132,9 @@ export const p5Versions = [
   '0.2.1'
 ];
 
-export const p5SoundURL =
-  'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js';
+export const currentP5Version = p5Versions[0];
+
+export const p5SoundURL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/addons/p5.sound.min.js`;
 export const p5PreloadAddonURL = 'https://TODO/preload.js';
 export const p5ShapesAddonURL = 'https://TODO/shapes.js';
 
@@ -196,7 +197,12 @@ export function useP5Version() {
 
       const p5SoundNode = [
         ...dom.documentElement.querySelectorAll('script')
-      ].find((s) => s.getAttribute('src') === p5SoundURL);
+      ].find((s) =>
+        [
+          /^https?:\/\/cdnjs.cloudflare.com\/ajax\/libs\/p5.js\/(.+)\/addons\/p5\.sound\.min\.js$/,
+          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5@(.+)\/lib\/addons\/p5\.sound\.min\.js$/
+        ].some((regex) => regex.exec(s.getAttribute('src') || ''))
+      );
       const setP5Sound = function (enabled) {
         if (!enabled && p5SoundNode) {
           p5SoundNode.parentNode.removeChild(p5SoundNode);
@@ -208,6 +214,17 @@ export function useP5Version() {
         return serializeResult();
       };
 
+      const setP5SoundURL = function (url) {
+        if (p5SoundNode) {
+          p5SoundNode.setAttribute('src', url);
+        } else {
+          const newNode = document.createElement('script');
+          newNode.setAttribute('src', url);
+          scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
+        }
+        return serializeResult();
+      };
+
       const p5PreloadAddonNode = [
         ...dom.documentElement.querySelectorAll('script')
       ].find((s) => s.getAttribute('src') === p5PreloadAddonURL);
@@ -242,6 +259,8 @@ export function useP5Version() {
         replaceVersion,
         p5Sound: !!p5SoundNode,
         setP5Sound,
+        setP5SoundURL,
+        p5SoundURL: p5SoundNode?.getAttribute('src'),
         p5PreloadAddon: !!p5PreloadAddonNode,
         setP5PreloadAddon,
         p5ShapesAdddon: !!p5ShapesAddonNode,
diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss
index 277f70fce4..42a7bfb350 100644
--- a/client/styles/components/_preferences.scss
+++ b/client/styles/components/_preferences.scss
@@ -174,6 +174,7 @@ input[type="number"]::-webkit-outer-spin-button {
 .preference__options {
   display: flex;
   justify-content: center;
+  align-items: center;
 }
 
 .preference__radio-button:checked + .preference__option {
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index adcf17f461..473e45b5a1 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -219,7 +219,8 @@
     "CustomVersionReset": "If you do want to use the default libraries, you can replace your script tags in index.html to the following:",
     "SoundAddon": "p5.sound.js Addon",
     "PreloadAddon": "p5.js 2.0 Addon - Preload",
-    "ShapesAddon": "p5.js 2.0 Addon - Shapes"
+    "ShapesAddon": "p5.js 2.0 Addon - Shapes",
+    "UndoSoundVersion": "Put back previous p5.sound.js version"
   },
   "KeyboardShortcuts": {
     "Title": " Keyboard Shortcuts",

From b209f84664ac490273c458aeba5b063fcc1fa6c5 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Thu, 27 Feb 2025 18:21:32 -0500
Subject: [PATCH 12/34] Move useP5Version into a provider to persist state

---
 .../IDE/components/Preferences/index.jsx      |  11 +-
 .../{useP5Version.js => useP5Version.jsx}     |  27 ++-
 client/modules/IDE/pages/IDEView.jsx          | 191 +++++++++---------
 3 files changed, 127 insertions(+), 102 deletions(-)
 rename client/modules/IDE/hooks/{useP5Version.js => useP5Version.jsx} (92%)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index ea4c0b289a..47697e5dc8 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -46,7 +46,6 @@ export default function Preferences() {
 
   const [state, setState] = useState({ fontSize });
   const { versionInfo, indexID } = useP5Version();
-  const [lastP5SoundURL, setLastP5SoundURL] = useState(undefined);
   const cmRef = useContext(CmControllerContext);
 
   function onFontInputChange(event) {
@@ -533,7 +532,7 @@ export default function Preferences() {
                       // default version that one will get via this toggle,
                       // record it so we can give the option to put it back
                       if (versionInfo.p5SoundURL !== p5SoundURL) {
-                        setLastP5SoundURL(versionInfo.p5SoundURL);
+                        versionInfo.setLastP5SoundURL(versionInfo.p5SoundURL);
                       }
                       updateHTML(versionInfo.setP5Sound(false));
                     }}
@@ -550,11 +549,13 @@ export default function Preferences() {
                   >
                     {t('Preferences.Off')}
                   </label>
-                  {lastP5SoundURL && (
+                  {versionInfo.lastP5SoundURL && (
                     <Button
                       onClick={() => {
-                        updateHTML(versionInfo.setP5SoundURL(lastP5SoundURL));
-                        setLastP5SoundURL(undefined);
+                        updateHTML(
+                          versionInfo.setP5SoundURL(versionInfo.lastP5SoundURL)
+                        );
+                        versionInfo.setLastP5SoundURL(undefined);
                       }}
                     >
                       {t('Preferences.UndoSoundVersion')}
diff --git a/client/modules/IDE/hooks/useP5Version.js b/client/modules/IDE/hooks/useP5Version.jsx
similarity index 92%
rename from client/modules/IDE/hooks/useP5Version.js
rename to client/modules/IDE/hooks/useP5Version.jsx
index bfabebdfc2..dbb8510c2d 100644
--- a/client/modules/IDE/hooks/useP5Version.js
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -1,5 +1,6 @@
-import { useMemo } from 'react';
+import React, { useContext, useMemo, useState } from 'react';
 import { useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
 
 // Generated from https://www.npmjs.com/package/p5?activeTab=versions
 // Run this in the console:
@@ -138,7 +139,9 @@ export const p5SoundURL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${curren
 export const p5PreloadAddonURL = 'https://TODO/preload.js';
 export const p5ShapesAddonURL = 'https://TODO/shapes.js';
 
-export function useP5Version() {
+const P5VersionContext = React.createContext({});
+
+export function P5VersionProvider(props) {
   const files = useSelector((state) => state.files);
   const indexFile = files.find(
     (file) =>
@@ -149,6 +152,8 @@ export function useP5Version() {
   const indexSrc = indexFile?.content;
   const indexID = indexFile?.id;
 
+  const [lastP5SoundURL, setLastP5SoundURL] = useState(undefined);
+
   // { version: string, minified: boolean, replaceVersion: (version: string) => string } | null
   const versionInfo = useMemo(() => {
     if (!indexSrc) return null;
@@ -261,6 +266,8 @@ export function useP5Version() {
         setP5Sound,
         setP5SoundURL,
         p5SoundURL: p5SoundNode?.getAttribute('src'),
+        lastP5SoundURL,
+        setLastP5SoundURL,
         p5PreloadAddon: !!p5PreloadAddonNode,
         setP5PreloadAddon,
         p5ShapesAdddon: !!p5ShapesAddonNode,
@@ -270,5 +277,19 @@ export function useP5Version() {
     return null;
   }, [indexSrc]);
 
-  return { indexID, versionInfo };
+  const value = { indexID, versionInfo };
+
+  return (
+    <P5VersionContext.Provider value={value}>
+      {props.children}
+    </P5VersionContext.Provider>
+  );
+}
+
+P5VersionProvider.propTypes = {
+  children: PropTypes.element.isRequired
+};
+
+export function useP5Version() {
+  return useContext(P5VersionContext);
 }
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index 2c6ed4a496..a9cc72f169 100644
--- a/client/modules/IDE/pages/IDEView.jsx
+++ b/client/modules/IDE/pages/IDEView.jsx
@@ -27,6 +27,7 @@ import {
 } from '../components/Editor/MobileEditor';
 import IDEOverlays from '../components/IDEOverlays';
 import useIsMobile from '../hooks/useIsMobile';
+import { P5VersionProvider } from '../hooks/useP5Version';
 
 function getTitle(project) {
   const { id } = project;
@@ -173,117 +174,119 @@ const IDEView = () => {
       <IDEKeyHandlers getContent={() => cmRef.current?.getContent()} />
       <WarnIfUnsavedChanges />
       <Toast />
-      <CmControllerContext.Provider value={cmRef}>
-        <Header syncFileContent={syncFileContent} />
-      </CmControllerContext.Provider>
-      {isMobile ? (
-        <>
-          <FloatingActionButton
-            syncFileContent={syncFileContent}
-            offsetBottom={ide.isPlaying ? currentConsoleSize : 0}
-          />
-          <PreviewWrapper show={ide.isPlaying}>
-            <SplitPane
-              style={{ position: 'static' }}
-              split="horizontal"
-              primary="second"
-              size={currentConsoleSize}
-              minSize={consoleCollapsedSize}
-              onChange={(size) => {
-                setConsoleSize(size);
-                setIsOverlayVisible(true);
-              }}
-              onDragFinished={() => {
-                setIsOverlayVisible(false);
-              }}
-              allowResize={ide.consoleIsExpanded}
-              className="editor-preview-subpanel"
-            >
-              <PreviewFrame
-                fullView
-                hide={!ide.isPlaying}
-                cmController={cmRef.current}
-                isOverlayVisible={isOverlayVisible}
-              />
-              <Console />
-            </SplitPane>
-          </PreviewWrapper>
-          <EditorSidebarWrapper show={!ide.isPlaying}>
-            <Sidebar />
-            <Editor
-              provideController={(ctl) => {
-                cmRef.current = ctl;
-              }}
+      <P5VersionProvider>
+        <CmControllerContext.Provider value={cmRef}>
+          <Header syncFileContent={syncFileContent} />
+        </CmControllerContext.Provider>
+        {isMobile ? (
+          <>
+            <FloatingActionButton
+              syncFileContent={syncFileContent}
+              offsetBottom={ide.isPlaying ? currentConsoleSize : 0}
             />
-          </EditorSidebarWrapper>
-        </>
-      ) : (
-        <main className="editor-preview-container">
-          <SplitPane
-            split="vertical"
-            size={ide.sidebarIsExpanded ? sidebarSize : 20}
-            onChange={(size) => {
-              setSidebarSize(size);
-            }}
-            allowResize={ide.sidebarIsExpanded}
-            minSize={150}
-          >
-            <Sidebar />
-            <SplitPane
-              split="vertical"
-              maxSize={MaxSize * 0.965}
-              defaultSize="50%"
-              onChange={() => {
-                setIsOverlayVisible(true);
-              }}
-              onDragFinished={() => {
-                setIsOverlayVisible(false);
-              }}
-              resizerStyle={{
-                borderLeftWidth: '2px',
-                borderRightWidth: '2px',
-                width: '2px',
-                margin: '0px 0px'
-              }}
-            >
+            <PreviewWrapper show={ide.isPlaying}>
               <SplitPane
+                style={{ position: 'static' }}
                 split="horizontal"
                 primary="second"
                 size={currentConsoleSize}
                 minSize={consoleCollapsedSize}
                 onChange={(size) => {
                   setConsoleSize(size);
+                  setIsOverlayVisible(true);
+                }}
+                onDragFinished={() => {
+                  setIsOverlayVisible(false);
                 }}
                 allowResize={ide.consoleIsExpanded}
                 className="editor-preview-subpanel"
               >
-                <Editor
-                  provideController={(ctl) => {
-                    cmRef.current = ctl;
-                  }}
+                <PreviewFrame
+                  fullView
+                  hide={!ide.isPlaying}
+                  cmController={cmRef.current}
+                  isOverlayVisible={isOverlayVisible}
                 />
                 <Console />
               </SplitPane>
-              <section className="preview-frame-holder">
-                <header className="preview-frame__header">
-                  <h2 className="preview-frame__title">
-                    {t('Toolbar.Preview')}
-                  </h2>
-                </header>
-                <div className="preview-frame__content">
-                  <PreviewFrame
-                    cmController={cmRef.current}
-                    isOverlayVisible={isOverlayVisible}
+            </PreviewWrapper>
+            <EditorSidebarWrapper show={!ide.isPlaying}>
+              <Sidebar />
+              <Editor
+                provideController={(ctl) => {
+                  cmRef.current = ctl;
+                }}
+              />
+            </EditorSidebarWrapper>
+          </>
+        ) : (
+          <main className="editor-preview-container">
+            <SplitPane
+              split="vertical"
+              size={ide.sidebarIsExpanded ? sidebarSize : 20}
+              onChange={(size) => {
+                setSidebarSize(size);
+              }}
+              allowResize={ide.sidebarIsExpanded}
+              minSize={150}
+            >
+              <Sidebar />
+              <SplitPane
+                split="vertical"
+                maxSize={MaxSize * 0.965}
+                defaultSize="50%"
+                onChange={() => {
+                  setIsOverlayVisible(true);
+                }}
+                onDragFinished={() => {
+                  setIsOverlayVisible(false);
+                }}
+                resizerStyle={{
+                  borderLeftWidth: '2px',
+                  borderRightWidth: '2px',
+                  width: '2px',
+                  margin: '0px 0px'
+                }}
+              >
+                <SplitPane
+                  split="horizontal"
+                  primary="second"
+                  size={currentConsoleSize}
+                  minSize={consoleCollapsedSize}
+                  onChange={(size) => {
+                    setConsoleSize(size);
+                  }}
+                  allowResize={ide.consoleIsExpanded}
+                  className="editor-preview-subpanel"
+                >
+                  <Editor
+                    provideController={(ctl) => {
+                      cmRef.current = ctl;
+                    }}
                   />
-                </div>
-              </section>
+                  <Console />
+                </SplitPane>
+                <section className="preview-frame-holder">
+                  <header className="preview-frame__header">
+                    <h2 className="preview-frame__title">
+                      {t('Toolbar.Preview')}
+                    </h2>
+                  </header>
+                  <div className="preview-frame__content">
+                    <PreviewFrame
+                      cmController={cmRef.current}
+                      isOverlayVisible={isOverlayVisible}
+                    />
+                  </div>
+                </section>
+              </SplitPane>
             </SplitPane>
-          </SplitPane>
-        </main>
-      )}
-      <CmControllerContext.Provider value={cmRef}>
-        <IDEOverlays />
-      </CmControllerContext.Provider>
+          </main>
+        )}
+        <CmControllerContext.Provider value={cmRef}>
+          <IDEOverlays />
+        </CmControllerContext.Provider>
+      </P5VersionProvider>
     </RootPage>
   );
 };

From 22dee375fe48f6a42ca139886249c65c4c3b565f Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Tue, 11 Mar 2025 13:27:44 -0400
Subject: [PATCH 13/34] Add starburst on change to 2.0

---
 .../modules/IDE/components/Header/Toolbar.jsx |  4 --
 .../IDE/components/Preferences/index.jsx      | 19 ++++++-
 client/modules/IDE/components/Stars.jsx       | 51 +++++++++++++++++++
 .../modules/IDE/components/VersionPicker.jsx  | 23 +++++++--
 client/styles/components/_stars.scss          | 28 ++++++++++
 client/styles/main.scss                       |  1 +
 6 files changed, 116 insertions(+), 10 deletions(-)
 create mode 100644 client/modules/IDE/components/Stars.jsx
 create mode 100644 client/styles/components/_stars.scss

diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx
index a15416011a..5ebdbdafea 100644
--- a/client/modules/IDE/components/Header/Toolbar.jsx
+++ b/client/modules/IDE/components/Header/Toolbar.jsx
@@ -20,7 +20,6 @@ import PlayIcon from '../../../../images/play.svg';
 import StopIcon from '../../../../images/stop.svg';
 import PreferencesIcon from '../../../../images/preferences.svg';
 import ProjectName from './ProjectName';
-import VersionPicker from '../VersionPicker';
 
 const Toolbar = (props) => {
   const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector(
@@ -98,9 +97,6 @@ const Toolbar = (props) => {
           {t('Toolbar.Auto-refresh')}
         </label>
       </div>
-      <div className="toolbar__version-picker">
-        <VersionPicker />
-      </div>
       <div className="toolbar__project-name-container">
         <ProjectName />
         {(() => {
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 47697e5dc8..65da36447f 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -24,6 +24,7 @@ import VersionPicker from '../VersionPicker';
 import { updateFileContent } from '../../actions/files';
 import { CmControllerContext } from '../../pages/IDEView';
 import Button from '../../../../common/Button';
+import Stars from '../Stars';
 
 export default function Preferences() {
   const { t } = useTranslation();
@@ -47,6 +48,18 @@ export default function Preferences() {
   const [state, setState] = useState({ fontSize });
   const { versionInfo, indexID } = useP5Version();
   const cmRef = useContext(CmControllerContext);
+  const [showStars, setShowStars] = useState(null);
+  const timerRef = useRef(null);
+  const pickerRef = useRef(null);
+  const onChangeVersion = (version) => {
+    const shouldShowStars = version.startsWith('2.');
+    const box = pickerRef.current?.getBoundingClientRect();
+    if (shouldShowStars) {
+      setShowStars({ left: box?.left || 0, top: box?.top || 0 });
+      clearTimeout(timerRef.current);
+      timerRef.current = setTimeout(() => setShowStars(null), 3000);
+    }
+  };
 
   function onFontInputChange(event) {
     const INTEGER_REGEX = /^[0-9\b]+$/;
@@ -484,7 +497,11 @@ export default function Preferences() {
               {t('Preferences.LibraryVersion')}
             </h4>
             <div>
-              <VersionPicker />
+              {showStars && <Stars top={showStars.top} left={showStars.left} />}
+              <VersionPicker
+                ref={pickerRef}
+                onChangeVersion={onChangeVersion}
+              />
               {versionInfo && indexID ? (
                 <p className="preference__paragraph">
                   {t('Preferences.LibraryVersionInfo')}
diff --git a/client/modules/IDE/components/Stars.jsx b/client/modules/IDE/components/Stars.jsx
new file mode 100644
index 0000000000..5807c37338
--- /dev/null
+++ b/client/modules/IDE/components/Stars.jsx
@@ -0,0 +1,51 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+import AsteriskIcon from '../../../images/p5-asterisk.svg';
+
+const Stars = ({ top, left }) => {
+  const stars = useMemo(() => {
+    const styles = [];
+
+    for (let i = 0; i < 20; i += 1) {
+      const x = Math.round(Math.random() * 200 - 100);
+      const y = Math.round(Math.random() * 200 - 100);
+      const s = 3 + Math.random() * 2;
+      const rotation = Math.random() * Math.PI * 2;
+      const style = {
+        transform: `translate(${x}px, ${y}px) scale(${s.toFixed(
+          4
+        )}) rotate(${rotation.toFixed(4)}rad)`
+      };
+      const key = i;
+      styles.push({ style, key });
+    }
+
+    return styles;
+  }, []);
+
+  return (
+    <div
+      className="stars"
+      style={{
+        top,
+        left
+      }}
+    >
+      {stars.map(({ style, key }) => (
+        <AsteriskIcon
+          key={key}
+          className="stars__star"
+          style={style}
+          focusable="false"
+        />
+      ))}
+    </div>
+  );
+};
+
+Stars.propTypes = {
+  top: PropTypes.number.isRequired,
+  left: PropTypes.number.isRequired
+};
+
+export default Stars;
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 331734436f..3e913ba023 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -1,6 +1,7 @@
 import React, { useCallback, useContext } from 'react';
 import { useDispatch } from 'react-redux';
 import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
 
 import { useP5Version, p5Versions } from '../hooks/useP5Version';
 import MenuItem from '../../../components/Dropdown/MenuItem';
@@ -8,7 +9,7 @@ import DropdownMenu from '../../../components/Dropdown/DropdownMenu';
 import { updateFileContent } from '../actions/files';
 import { CmControllerContext } from '../pages/IDEView';
 
-const VersionPicker = () => {
+const VersionPicker = React.forwardRef(({ onChangeVersion }, ref) => {
   const { indexID, versionInfo } = useP5Version();
   const { t } = useTranslation();
   const dispatch = useDispatch();
@@ -16,11 +17,14 @@ const VersionPicker = () => {
   const dispatchReplaceVersion = useCallback(
     (version) => {
       if (!indexID || !versionInfo) return;
+      if (onChangeVersion) {
+        onChangeVersion(version);
+      }
       const src = versionInfo.replaceVersion(version);
       dispatch(updateFileContent(indexID, src));
       cmRef.current?.updateFileContent(indexID, src);
     },
-    [indexID, versionInfo, cmRef]
+    [indexID, versionInfo, cmRef, onChangeVersion]
   );
 
   if (!versionInfo) {
@@ -31,7 +35,11 @@ const VersionPicker = () => {
 
   return (
     <DropdownMenu
-      anchor={<span className="versionPicker">{versionInfo.version}</span>}
+      anchor={
+        <span className="versionPicker" ref={ref}>
+          {versionInfo.version}
+        </span>
+      }
       align="left"
     >
       {p5Versions.map((version) => (
@@ -41,8 +49,13 @@ const VersionPicker = () => {
       ))}
     </DropdownMenu>
   );
-};
+});
 
-VersionPicker.propTypes = {};
+VersionPicker.propTypes = {
+  onChangeVersion: PropTypes.func
+};
+VersionPicker.defaultProps = {
+  onChangeVersion: undefined
+};
 
 export default VersionPicker;
diff --git a/client/styles/components/_stars.scss b/client/styles/components/_stars.scss
new file mode 100644
index 0000000000..fcfd3dfd31
--- /dev/null
+++ b/client/styles/components/_stars.scss
@@ -0,0 +1,28 @@
+@keyframes starburst {
+  from {
+    transform: translate(0, 0) scale(0) rotate(0);
+  }
+  to {
+    opacity: 0;
+  }
+}
+
+.stars {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 9000;
+  width: 0;
+  height: 0;
+  pointer-events: none;
+}
+
+.stars__star {
+  position: absolute;
+  left: 0;
+  top: 0;
+  transform-origin: 50% 50%;
+  animation: starburst 1.5s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+}
diff --git a/client/styles/main.scss b/client/styles/main.scss
index c82a2c3e8f..cb88a534de 100644
--- a/client/styles/main.scss
+++ b/client/styles/main.scss
@@ -53,6 +53,7 @@
 @import 'components/collection-create';
 @import 'components/quick-add';
 @import 'components/skip-link';
+@import 'components/stars';
 
 @import 'layout/dashboard';
 @import 'layout/ide';

From d85b7d592a05ffd78bab27a475465ed1256c7333 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 12 Mar 2025 15:56:08 -0400
Subject: [PATCH 14/34] Make toggling on restore instead of a button

---
 .../IDE/components/Preferences/index.jsx      | 26 +++++++++++--------
 client/styles/abstracts/_variables.scss       |  3 +++
 client/styles/components/_preferences.scss    |  7 +++++
 translations/locales/en-US/translations.json  |  2 +-
 4 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 65da36447f..f0d2331d07 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -23,7 +23,6 @@ import { p5SoundURL, useP5Version } from '../../hooks/useP5Version';
 import VersionPicker from '../VersionPicker';
 import { updateFileContent } from '../../actions/files';
 import { CmControllerContext } from '../../pages/IDEView';
-import Button from '../../../../common/Button';
 import Stars from '../Stars';
 
 export default function Preferences() {
@@ -531,7 +530,19 @@ export default function Preferences() {
                 <div className="preference__options">
                   <input
                     type="radio"
-                    onChange={() => updateHTML(versionInfo.setP5Sound(true))}
+                    onChange={() => {
+                      if (versionInfo.lastP5SoundURL) {
+                        // If the sketch previously used a nonstandard p5.sound
+                        // URL, restore that URL
+                        updateHTML(
+                          versionInfo.setP5SoundURL(versionInfo.lastP5SoundURL)
+                        );
+                        versionInfo.setLastP5SoundURL(undefined);
+                      } else {
+                        // Otherwise, turn on the default p5.sound URL
+                        updateHTML(versionInfo.setP5Sound(true));
+                      }
+                    }}
                     aria-label={t('Preferences.AutosaveOnARIA')}
                     name="soundaddon"
                     id="soundaddon-on"
@@ -567,16 +578,9 @@ export default function Preferences() {
                     {t('Preferences.Off')}
                   </label>
                   {versionInfo.lastP5SoundURL && (
-                    <Button
-                      onClick={() => {
-                        updateHTML(
-                          versionInfo.setP5SoundURL(versionInfo.lastP5SoundURL)
-                        );
-                        versionInfo.setLastP5SoundURL(undefined);
-                      }}
-                    >
+                    <span className="preference__warning">
                       {t('Preferences.UndoSoundVersion')}
-                    </Button>
+                    </span>
                   )}
                 </div>
               </div>
diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss
index 17cd04bab8..0f6035a410 100644
--- a/client/styles/abstracts/_variables.scss
+++ b/client/styles/abstracts/_variables.scss
@@ -86,6 +86,7 @@ $themes: (
     table-row-stripe-color-alternate: $medium-light,
     codefold-icon-open: url("../images/triangle-arrow-down.svg?byUrl"),
     codefold-icon-closed: url("../images/triangle-arrow-right.svg?byUrl"),
+    preferences-warning-color: $p5js-pink,
 
     table-button-color: $lightest,
     table-button-background-color: $middle-gray,
@@ -182,6 +183,7 @@ $themes: (
     table-row-stripe-color-alternate: $darker,
     codefold-icon-open: url("../images/triangle-arrow-down-white.svg?byUrl"),
     codefold-icon-closed: url("../images/triangle-arrow-right-white.svg?byUrl"),
+    preferences-warning-color: $yellow,
 
     table-button-color: $lightest,
     table-button-background-color: $middle-gray,
@@ -276,6 +278,7 @@ $themes: (
     table-row-stripe-color-alternate: $darker,
     codefold-icon-open: url("../images/triangle-arrow-down-white.svg?byUrl"),
     codefold-icon-closed: url("../images/triangle-arrow-right-white.svg?byUrl"),
+    preferences-warning-color: $yellow,
 
     table-button-color: $dark,
     table-button-background-color: $middle-gray,
diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss
index 42a7bfb350..ccbc5d1e01 100644
--- a/client/styles/components/_preferences.scss
+++ b/client/styles/components/_preferences.scss
@@ -137,6 +137,13 @@
   }
 }
 
+.preference__warning {
+  @include themify() {
+    font-weight: bold;
+    color: getThemifyVariable("preferences-warning-color");
+  }
+}
+
 .preference__radio-button {
   @extend %hidden-element;
 }
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 473e45b5a1..262694e54e 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -220,7 +220,7 @@
     "SoundAddon": "p5.sound.js Addon",
     "PreloadAddon": "p5.js 2.0 Addon - Preload",
     "ShapesAddon": "p5.js 2.0 Addon - Shapes",
-    "UndoSoundVersion": "Put back previous p5.sound.js version"
+    "UndoSoundVersion": "Want to use p5.sound.js again? Turning it back on will restore the version you were using before."
   },
   "KeyboardShortcuts": {
     "Title": " Keyboard Shortcuts",

From d6ff5accfa147d83871ea85c32a5eac1fb0374fa Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 12 Mar 2025 16:02:29 -0400
Subject: [PATCH 15/34] Add max height to version picker menu

---
 client/components/Dropdown/DropdownMenu.jsx   | 19 +++++++++++++++----
 .../modules/IDE/components/VersionPicker.jsx  |  1 +
 2 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx
index 3371d1d6cc..ddaa1c338a 100644
--- a/client/components/Dropdown/DropdownMenu.jsx
+++ b/client/components/Dropdown/DropdownMenu.jsx
@@ -8,7 +8,15 @@ import { DropdownWrapper } from '../Dropdown';
 
 const DropdownMenu = forwardRef(
   (
-    { children, anchor, 'aria-label': ariaLabel, align, className, classes },
+    {
+      children,
+      anchor,
+      'aria-label': ariaLabel,
+      align,
+      className,
+      classes,
+      maxHeight
+    },
     ref
   ) => {
     // Note: need to use a ref instead of a state to avoid stale closures.
@@ -32,7 +40,7 @@ const DropdownMenu = forwardRef(
       focusedRef.current = false;
       setTimeout(() => {
         if (!focusedRef.current) {
-          close();
+          // close();
         }
       }, 200);
     };
@@ -59,6 +67,7 @@ const DropdownMenu = forwardRef(
             }}
             onBlur={handleBlur}
             onFocus={handleFocus}
+            style={maxHeight && { maxHeight, overflowY: 'auto' }}
           >
             {children}
           </DropdownWrapper>
@@ -84,14 +93,16 @@ DropdownMenu.propTypes = {
   classes: PropTypes.shape({
     button: PropTypes.string,
     list: PropTypes.string
-  })
+  }),
+  maxHeight: PropTypes.string
 };
 
 DropdownMenu.defaultProps = {
   anchor: null,
   align: 'right',
   className: '',
-  classes: {}
+  classes: {},
+  maxHeight: undefined
 };
 
 export default DropdownMenu;
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index 3e913ba023..b1250204b1 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -41,6 +41,7 @@ const VersionPicker = React.forwardRef(({ onChangeVersion }, ref) => {
         </span>
       }
       align="left"
+      maxHeight="50vh"
     >
       {p5Versions.map((version) => (
         <MenuItem key={version} onClick={() => dispatchReplaceVersion(version)}>

From aeb8782ae12320ad430cf89ed066bb13df02dd38 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 12 Mar 2025 16:04:55 -0400
Subject: [PATCH 16/34] Make p5.sound regex more permissive

---
 client/modules/IDE/hooks/useP5Version.jsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index dbb8510c2d..9b673dd562 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -204,8 +204,8 @@ export function P5VersionProvider(props) {
         ...dom.documentElement.querySelectorAll('script')
       ].find((s) =>
         [
-          /^https?:\/\/cdnjs.cloudflare.com\/ajax\/libs\/p5.js\/(.+)\/addons\/p5\.sound\.min\.js$/,
-          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5@(.+)\/lib\/addons\/p5\.sound\.min\.js$/
+          /^https?:\/\/cdnjs.cloudflare.com\/ajax\/libs\/p5.js\/(.+)\/addons\/p5\.sound(?:\.min)?\.js$/,
+          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5@(.+)\/lib\/addons\/p5\.sound(?:\.min)?\.js$/
         ].some((regex) => regex.exec(s.getAttribute('src') || ''))
       );
       const setP5Sound = function (enabled) {

From c1f9dce35e4289027e5b2c9bf19e0501e27b829c Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 15 Mar 2025 13:46:09 -0400
Subject: [PATCH 17/34] Add admonition and copy to clipboard button

---
 client/common/icons.jsx                       |  2 +
 client/images/copy.svg                        |  6 ++
 client/modules/IDE/components/Admonition.jsx  | 26 ++++++++
 .../IDE/components/Preferences/index.jsx      | 59 +++++++++++--------
 client/modules/IDE/components/TextArea.jsx    | 48 +++++++++++++++
 client/styles/abstracts/_variables.scss       | 12 ++++
 client/styles/components/_admonition.scss     | 20 +++++++
 client/styles/components/_preferences.scss    |  1 +
 client/styles/main.scss                       |  1 +
 translations/locales/en-US/translations.json  | 11 +++-
 10 files changed, 157 insertions(+), 29 deletions(-)
 create mode 100644 client/images/copy.svg
 create mode 100644 client/modules/IDE/components/Admonition.jsx
 create mode 100644 client/modules/IDE/components/TextArea.jsx
 create mode 100644 client/styles/components/_admonition.scss

diff --git a/client/common/icons.jsx b/client/common/icons.jsx
index 13d2e18092..1184945dea 100644
--- a/client/common/icons.jsx
+++ b/client/common/icons.jsx
@@ -25,6 +25,7 @@ import CircleInfo from '../images/circle-info.svg';
 import Add from '../images/add.svg';
 import Filter from '../images/filter.svg';
 import Cross from '../images/cross.svg';
+import Copy from '../images/copy.svg';
 
 // HOC that adds the right web accessibility props
 // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
@@ -102,3 +103,4 @@ export const CircleFolderIcon = withLabel(CircleFolder);
 export const CircleInfoIcon = withLabel(CircleInfo);
 export const AddIcon = withLabel(Add);
 export const FilterIcon = withLabel(Filter);
+export const CopyIcon = withLabel(Copy);
diff --git a/client/images/copy.svg b/client/images/copy.svg
new file mode 100644
index 0000000000..cde8328433
--- /dev/null
+++ b/client/images/copy.svg
@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g>
+<path d="M0 5.90625C0 5.061 0.686 4.375 1.53125 4.375H2.84375C3.0178 4.375 3.18472 4.44414 3.30779 4.56721C3.43086 4.69028 3.5 4.8572 3.5 5.03125C3.5 5.2053 3.43086 5.37222 3.30779 5.49529C3.18472 5.61836 3.0178 5.6875 2.84375 5.6875H1.53125C1.47323 5.6875 1.41759 5.71055 1.37657 5.75157C1.33555 5.79259 1.3125 5.84823 1.3125 5.90625V12.4688C1.3125 12.5895 1.4105 12.6875 1.53125 12.6875H8.09375C8.15177 12.6875 8.20741 12.6645 8.24843 12.6234C8.28945 12.5824 8.3125 12.5268 8.3125 12.4688V11.1562C8.3125 10.9822 8.38164 10.8153 8.50471 10.6922C8.62778 10.5691 8.7947 10.5 8.96875 10.5C9.1428 10.5 9.30972 10.5691 9.43279 10.6922C9.55586 10.8153 9.625 10.9822 9.625 11.1562V12.4688C9.625 12.8749 9.46367 13.2643 9.17651 13.5515C8.88934 13.8387 8.49986 14 8.09375 14H1.53125C1.12514 14 0.735658 13.8387 0.448493 13.5515C0.161328 13.2643 0 12.8749 0 12.4688L0 5.90625Z" fill="currentColor"/>
+<path d="M4.375 1.53125C4.375 0.686 5.061 0 5.90625 0H12.4688C13.314 0 14 0.686 14 1.53125V8.09375C14 8.49986 13.8387 8.88934 13.5515 9.17651C13.2643 9.46367 12.8749 9.625 12.4688 9.625H5.90625C5.50014 9.625 5.11066 9.46367 4.82349 9.17651C4.53633 8.88934 4.375 8.49986 4.375 8.09375V1.53125ZM5.90625 1.3125C5.84823 1.3125 5.79259 1.33555 5.75157 1.37657C5.71055 1.41759 5.6875 1.47323 5.6875 1.53125V8.09375C5.6875 8.2145 5.7855 8.3125 5.90625 8.3125H12.4688C12.5268 8.3125 12.5824 8.28945 12.6234 8.24843C12.6645 8.20741 12.6875 8.15177 12.6875 8.09375V1.53125C12.6875 1.47323 12.6645 1.41759 12.6234 1.37657C12.5824 1.33555 12.5268 1.3125 12.4688 1.3125H5.90625Z" fill="currentColor"/>
+</g>
+</svg>
diff --git a/client/modules/IDE/components/Admonition.jsx b/client/modules/IDE/components/Admonition.jsx
new file mode 100644
index 0000000000..28e057cfad
--- /dev/null
+++ b/client/modules/IDE/components/Admonition.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+export default function Admonition({ children }) {
+  const { t } = useTranslation();
+  return (
+    <div className="admonition">
+      <h3 className="admonition__title">
+        <span role="img" aria-label="Flower" className="admonition__icon">
+          🌸
+        </span>
+        {t('Admonition.Note')}
+      </h3>
+      {children}
+    </div>
+  );
+}
+
+Admonition.propTypes = {
+  children: PropTypes.node
+};
+
+Admonition.defaultProps = {
+  children: undefined
+};
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index f0d2331d07..1506728caa 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -24,6 +24,8 @@ import VersionPicker from '../VersionPicker';
 import { updateFileContent } from '../../actions/files';
 import { CmControllerContext } from '../../pages/IDEView';
 import Stars from '../Stars';
+import Admonition from '../Admonition';
+import TextArea from '../TextArea';
 
 export default function Preferences() {
   const { t } = useTranslation();
@@ -492,34 +494,39 @@ export default function Preferences() {
         </TabPanel>
         <TabPanel>
           <div className="preference">
-            <h4 className="preference__title">
-              {t('Preferences.LibraryVersion')}
-            </h4>
-            <div>
-              {showStars && <Stars top={showStars.top} left={showStars.left} />}
-              <VersionPicker
-                ref={pickerRef}
-                onChangeVersion={onChangeVersion}
-              />
-              {versionInfo && indexID ? (
-                <p className="preference__paragraph">
-                  {t('Preferences.LibraryVersionInfo')}
-                </p>
-              ) : (
-                <>
-                  <p className="preference__paragraph">
-                    {t('Preferences.CustomVersionInfo')}
-                  </p>
+            {showStars && <Stars top={showStars.top} left={showStars.left} />}
+            {versionInfo && indexID ? (
+              <>
+                <h4 className="preference__title">
+                  {t('Preferences.LibraryVersion')}
+                </h4>
+                <div>
+                  <VersionPicker
+                    ref={pickerRef}
+                    onChangeVersion={onChangeVersion}
+                  />
                   <p className="preference__paragraph">
-                    {t('Preferences.CustomVersionReset')}
+                    {t('Preferences.LibraryVersionInfo')}
                   </p>
-                  <textarea className="preference__textarea">
-                    {'<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>\n' +
-                      '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js"></script>'}
-                  </textarea>
-                </>
-              )}
-            </div>
+                </div>
+              </>
+            ) : (
+              <div>
+                <Admonition>
+                  <p>{t('Preferences.CustomVersionInfo')}</p>
+                </Admonition>
+                <p className="preference__paragraph">
+                  {t('Preferences.CustomVersionReset')}
+                </p>
+                <TextArea
+                  className="preference__textarea"
+                  src={
+                    '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>\n' +
+                    '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js"></script>'
+                  }
+                />
+              </div>
+            )}
           </div>
           {versionInfo && indexID && (
             <>
diff --git a/client/modules/IDE/components/TextArea.jsx b/client/modules/IDE/components/TextArea.jsx
new file mode 100644
index 0000000000..f46d91d2b9
--- /dev/null
+++ b/client/modules/IDE/components/TextArea.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { CopyIcon } from '../../../common/icons';
+import { showToast } from '../actions/toast';
+
+const TextAreaWrapper = styled.div`
+  position: relative;
+`;
+
+const CornerButton = styled.button`
+  position: absolute;
+  top: 0.5rem;
+  right: 0.5rem;
+`;
+
+export default function TextArea({ src, className }) {
+  const { t } = useTranslation();
+  const dispatch = useDispatch();
+  const copyTextToClipboard = async () => {
+    try {
+      await navigator.clipboard.writeText(src);
+      dispatch(showToast(t('Preferences.CopyToClipboardSuccess')));
+    } catch (_e) {
+      dispatch(showToast(t('Preferences.CopyToClipboardFailure'), 5000));
+    }
+  };
+
+  return (
+    <TextAreaWrapper>
+      <textarea className={className}>{src}</textarea>
+      <CornerButton onClick={copyTextToClipboard}>
+        <CopyIcon aria-label="Copy" />
+      </CornerButton>
+    </TextAreaWrapper>
+  );
+}
+
+TextArea.propTypes = {
+  src: PropTypes.string.isRequired,
+  className: PropTypes.string
+};
+
+TextArea.defaultProps = {
+  className: undefined
+};
diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss
index 0f6035a410..b719506e92 100644
--- a/client/styles/abstracts/_variables.scss
+++ b/client/styles/abstracts/_variables.scss
@@ -126,6 +126,10 @@ $themes: (
     hint-item-active-outline-offset: 0,
     hint-inline-text-color-light: $middle-light,
     hint-inline-text-color: $middle-gray,
+
+    admonition-border: #ED225D,
+    admonition-background: #FFE4EC,
+    admonition-text: #AF0E3D,
   ),
   dark: (
     logo-color: $p5js-pink,
@@ -221,6 +225,10 @@ $themes: (
     hint-item-active-outline-offset: 0,
     hint-inline-text-color-light: $middle-gray,
     hint-inline-text-color: #cfcfcf,
+
+    admonition-border: #ED225D,
+    admonition-background: #FFE4EC,
+    admonition-text: #AF0E3D,
   ),
   contrast: (
     logo-color: $yellow,
@@ -316,6 +324,10 @@ $themes: (
     hint-item-active-outline-offset: -2px,
     hint-inline-text-color-light: $middle-gray,
     hint-inline-text-color: #cfcfcf,
+
+    admonition-border: #ED225D,
+    admonition-background: $white,
+    admonition-text: #AF0E3D,
   )
 );
 
diff --git a/client/styles/components/_admonition.scss b/client/styles/components/_admonition.scss
new file mode 100644
index 0000000000..8d2c366b73
--- /dev/null
+++ b/client/styles/components/_admonition.scss
@@ -0,0 +1,20 @@
+.admonition {
+  @include themify() {
+    padding: 1rem;
+    margin-bottom: 1rem;
+    margin-top: 1rem;
+    background: getThemifyVariable('admonition-background');
+    color: getThemifyVariable('admonition-text');
+    border: 1px solid getThemifyVariable('admonition-border');
+    border-left-width: 3px;
+  }
+}
+
+.admonition__icon {
+  margin-right: 0.5rem;
+}
+
+.admonition__title {
+  font-weight: bold;
+  margin-bottom: 0.5rem;
+}
diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss
index ccbc5d1e01..a5a352f864 100644
--- a/client/styles/components/_preferences.scss
+++ b/client/styles/components/_preferences.scss
@@ -72,6 +72,7 @@
 }
 
 .preference__textarea {
+  font-family: Inconsolata, monospace;
   width: 100%;
   min-height: 8em;
 }
diff --git a/client/styles/main.scss b/client/styles/main.scss
index cb88a534de..91e2d93483 100644
--- a/client/styles/main.scss
+++ b/client/styles/main.scss
@@ -54,6 +54,7 @@
 @import 'components/quick-add';
 @import 'components/skip-link';
 @import 'components/stars';
+@import 'components/admonition';
 
 @import 'layout/dashboard';
 @import 'layout/ide';
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 44d1f5580a..4a75b68682 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -218,12 +218,14 @@
     "TableOutputARIA": "table output on",
     "LibraryVersion": "p5.js Version",
     "LibraryVersionInfo": "TODO Add helpful info about the new p5.js version, compatibility, etc.",
-    "CustomVersionInfo": "It looks like you've changed the <script> tag to manage the version yourself. In that case, the version can't be managed from this tab, only from the code in index.html.",
-    "CustomVersionReset": "If you do want to use the default libraries, you can replace your script tags in index.html to the following:",
+    "CustomVersionInfo": "The version of p5.js is currently being managed in the code of index.html. This means it can't be adjusted from this tab.",
+    "CustomVersionReset": "If you'd like to use the default libraries, you can replace the script tags in index.html with the following:",
     "SoundAddon": "p5.sound.js Addon",
     "PreloadAddon": "p5.js 2.0 Addon - Preload",
     "ShapesAddon": "p5.js 2.0 Addon - Shapes",
-    "UndoSoundVersion": "Want to use p5.sound.js again? Turning it back on will restore the version you were using before."
+    "UndoSoundVersion": "Want to use p5.sound.js again? Turning it back on will restore the version you were using before.",
+    "CopyToClipboardSuccess": "Copied to clipboard!",
+    "CopyToClipboardFailure": "We weren't able to copy the text, try selecting it and copying it manually."
   },
   "KeyboardShortcuts": {
     "Title": " Keyboard Shortcuts",
@@ -664,5 +666,8 @@
   },
   "SkipLink": {
     "PlaySketch": "Skip to Play Sketch"
+  },
+  "Admonition": {
+    "Note": "Note"
   }
 }

From 2279d3a252cc75fadf73d8a498c87040bcd94b39 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 15 Mar 2025 13:49:14 -0400
Subject: [PATCH 18/34] Use latest p5/p5.sound in textarea

---
 client/modules/IDE/components/Preferences/index.jsx | 6 +++---
 client/modules/IDE/hooks/useP5Version.jsx           | 1 +
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 1506728caa..2dd53cf2a2 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -19,7 +19,7 @@ import {
   setLinewrap,
   setPreferencesTab
 } from '../../actions/preferences';
-import { p5SoundURL, useP5Version } from '../../hooks/useP5Version';
+import { p5SoundURL, p5URL, useP5Version } from '../../hooks/useP5Version';
 import VersionPicker from '../VersionPicker';
 import { updateFileContent } from '../../actions/files';
 import { CmControllerContext } from '../../pages/IDEView';
@@ -521,8 +521,8 @@ export default function Preferences() {
                 <TextArea
                   className="preference__textarea"
                   src={
-                    '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>\n' +
-                    '<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js"></script>'
+                    `<script src="${p5URL}"></script>\n` +
+                    `<script src="${p5SoundURL}"></script>`
                   }
                 />
               </div>
diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index 9b673dd562..29870ae160 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -138,6 +138,7 @@ export const currentP5Version = p5Versions[0];
 export const p5SoundURL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/addons/p5.sound.min.js`;
 export const p5PreloadAddonURL = 'https://TODO/preload.js';
 export const p5ShapesAddonURL = 'https://TODO/shapes.js';
+export const p5URL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/p5.js`;
 
 const P5VersionContext = React.createContext({});
 

From cae11eb34463d63300e3e75f27b7f7bbbb791ea8 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 15 Mar 2025 14:15:36 -0400
Subject: [PATCH 19/34] Style dropdown button

---
 .../modules/IDE/components/VersionPicker.jsx  | 55 +++++++++++++++++--
 1 file changed, 49 insertions(+), 6 deletions(-)

diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index b1250204b1..bb59382ad7 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -2,12 +2,44 @@ import React, { useCallback, useContext } from 'react';
 import { useDispatch } from 'react-redux';
 import { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
+import styled from 'styled-components';
 
+import { prop } from '../../../theme';
 import { useP5Version, p5Versions } from '../hooks/useP5Version';
 import MenuItem from '../../../components/Dropdown/MenuItem';
 import DropdownMenu from '../../../components/Dropdown/DropdownMenu';
 import { updateFileContent } from '../actions/files';
 import { CmControllerContext } from '../pages/IDEView';
+import { DropdownArrowIcon } from '../.././../common/icons';
+
+const VersionPickerButton = styled.div`
+  display: flex;
+  border: 1px solid ${prop('Modal.border')};
+  background: ${prop('backgroundColor')};
+`;
+
+const VersionPickerText = styled.div`
+  padding: 0.5rem 1rem;
+  min-width: 8rem;
+  text-align: left;
+`;
+
+const VersionPickerArrow = styled.div`
+  background: ${prop('Button.primary.default.background')};
+  align-self: stretch;
+  width: 2rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const VersionDropdownMenu = styled(DropdownMenu)`
+  & button {
+    padding: 0;
+  }
+
+  margin-bottom: 1rem;
+`;
 
 const VersionPicker = React.forwardRef(({ onChangeVersion }, ref) => {
   const { indexID, versionInfo } = useP5Version();
@@ -29,16 +61,27 @@ const VersionPicker = React.forwardRef(({ onChangeVersion }, ref) => {
 
   if (!versionInfo) {
     return (
-      <span className="versionPicker">{t('Toolbar.CustomLibraryVersion')}</span>
+      <VersionPickerButton>
+        <VersionPickerText>
+          {t('Toolbar.CustomLibraryVersion')}
+        </VersionPickerText>
+        <VersionPickerArrow>
+          <DropdownArrowIcon />
+        </VersionPickerArrow>
+      </VersionPickerButton>
     );
   }
 
   return (
-    <DropdownMenu
+    <VersionDropdownMenu
+      className="versionPicker"
       anchor={
-        <span className="versionPicker" ref={ref}>
-          {versionInfo.version}
-        </span>
+        <VersionPickerButton ref={ref}>
+          <VersionPickerText>{versionInfo.version}</VersionPickerText>
+          <VersionPickerArrow>
+            <DropdownArrowIcon />
+          </VersionPickerArrow>
+        </VersionPickerButton>
       }
       align="left"
       maxHeight="50vh"
@@ -48,7 +91,7 @@ const VersionPicker = React.forwardRef(({ onChangeVersion }, ref) => {
           {version}
         </MenuItem>
       ))}
-    </DropdownMenu>
+    </VersionDropdownMenu>
   );
 });
 

From 70fde43c47ddf0eec9b4017be4d5f6bb5dfd978f Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Mon, 17 Mar 2025 17:04:39 -0400
Subject: [PATCH 20/34] Update versioning note + admonition

---
 client/modules/IDE/components/Admonition.jsx  | 14 +++----
 .../IDE/components/Preferences/index.jsx      | 41 +++++++++++++++++--
 client/styles/abstracts/_variables.scss       | 18 ++++----
 client/styles/components/_preferences.scss    |  9 ++++
 translations/locales/en-US/translations.json  |  6 +--
 5 files changed, 62 insertions(+), 26 deletions(-)

diff --git a/client/modules/IDE/components/Admonition.jsx b/client/modules/IDE/components/Admonition.jsx
index 28e057cfad..8d9a6c6db1 100644
--- a/client/modules/IDE/components/Admonition.jsx
+++ b/client/modules/IDE/components/Admonition.jsx
@@ -1,23 +1,19 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
 
-export default function Admonition({ children }) {
-  const { t } = useTranslation();
+export default function Admonition({ children, title }) {
   return (
     <div className="admonition">
-      <h3 className="admonition__title">
-        <span role="img" aria-label="Flower" className="admonition__icon">
-          🌸
-        </span>
-        {t('Admonition.Note')}
-      </h3>
+      <p className="admonition__title">
+        <strong>{title}</strong>
+      </p>
       {children}
     </div>
   );
 }
 
 Admonition.propTypes = {
+  title: PropTypes.string.isRequired,
   children: PropTypes.node
 };
 
diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 2dd53cf2a2..daeddce97f 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -1,8 +1,10 @@
-import React, { useContext, useRef, useState } from 'react';
+import React, { useContext, useMemo, useRef, useState } from 'react';
 import { Helmet } from 'react-helmet';
 import { useDispatch, useSelector } from 'react-redux';
 import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
 import { useTranslation } from 'react-i18next';
+import ReactMarkdown from 'react-markdown';
+import PropTypes from 'prop-types';
 import PlusIcon from '../../../../images/plus.svg';
 import MinusIcon from '../../../../images/minus.svg';
 import beepUrl from '../../../../sounds/audioAlert.mp3';
@@ -112,6 +114,37 @@ export default function Preferences() {
     cmRef.current?.updateFileContent(indexID, src);
   };
 
+  const markdownComponents = useMemo(() => {
+    const ExternalLink = ({ children, ...props }) => (
+      <a {...props} target="_blank">
+        {children}
+      </a>
+    );
+    ExternalLink.propTypes = {
+      children: PropTypes.node
+    };
+    ExternalLink.defaultProps = {
+      children: undefined
+    };
+
+    const Paragraph = ({ children, ...props }) => (
+      <p className="preference__paragraph" {...props}>
+        {children}
+      </p>
+    );
+    Paragraph.propTypes = {
+      children: PropTypes.node
+    };
+    Paragraph.defaultProps = {
+      children: undefined
+    };
+
+    return {
+      a: ExternalLink,
+      p: Paragraph
+    };
+  }, []);
+
   return (
     <section className="preferences">
       <Helmet>
@@ -505,14 +538,14 @@ export default function Preferences() {
                     ref={pickerRef}
                     onChangeVersion={onChangeVersion}
                   />
-                  <p className="preference__paragraph">
+                  <ReactMarkdown components={markdownComponents}>
                     {t('Preferences.LibraryVersionInfo')}
-                  </p>
+                  </ReactMarkdown>
                 </div>
               </>
             ) : (
               <div>
-                <Admonition>
+                <Admonition title={t('Preferences.CustomVersionTitle')}>
                   <p>{t('Preferences.CustomVersionInfo')}</p>
                 </Admonition>
                 <p className="preference__paragraph">
diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss
index b719506e92..8ad2e57062 100644
--- a/client/styles/abstracts/_variables.scss
+++ b/client/styles/abstracts/_variables.scss
@@ -127,9 +127,9 @@ $themes: (
     hint-inline-text-color-light: $middle-light,
     hint-inline-text-color: $middle-gray,
 
-    admonition-border: #ED225D,
-    admonition-background: #FFE4EC,
-    admonition-text: #AF0E3D,
+    admonition-border: #22C8ED,
+    admonition-background: #E4F8FF,
+    admonition-text: #0E86A1,
   ),
   dark: (
     logo-color: $p5js-pink,
@@ -226,9 +226,9 @@ $themes: (
     hint-inline-text-color-light: $middle-gray,
     hint-inline-text-color: #cfcfcf,
 
-    admonition-border: #ED225D,
-    admonition-background: #FFE4EC,
-    admonition-text: #AF0E3D,
+    admonition-border: #22C8ED,
+    admonition-background: #1681b7,
+    admonition-text: #E4F8FF,
   ),
   contrast: (
     logo-color: $yellow,
@@ -325,9 +325,9 @@ $themes: (
     hint-inline-text-color-light: $middle-gray,
     hint-inline-text-color: #cfcfcf,
 
-    admonition-border: #ED225D,
-    admonition-background: $white,
-    admonition-text: #AF0E3D,
+    admonition-border: #22C8ED,
+    admonition-background: #1681b7,
+    admonition-text: #ffffff,
   )
 );
 
diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss
index a5a352f864..bbf2ce8979 100644
--- a/client/styles/components/_preferences.scss
+++ b/client/styles/components/_preferences.scss
@@ -69,6 +69,15 @@
 
 .preference__paragraph {
   margin-bottom: #{math.div(10, $base-font-size)}rem;
+
+  & a {
+    @include themify() {
+      color: getThemifyVariable('button-background-hover-color');
+    }
+  }
+  & a:hover {
+    text-decoration: underline;
+  }
 }
 
 .preference__textarea {
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 4a75b68682..0e20e0f58a 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -217,7 +217,8 @@
     "TableText": "Table-text",
     "TableOutputARIA": "table output on",
     "LibraryVersion": "p5.js Version",
-    "LibraryVersionInfo": "TODO Add helpful info about the new p5.js version, compatibility, etc.",
+    "LibraryVersionInfo": "There's a [new 2.0 release](https://github.com/processing/p5.js/releases/) of p5.js available! It will become default in August, 2026, so take this time to test it out and report bugs. Interested in transitioning sketches from 1.x to 2.0? Check out the [compatibility & transition resources.](https://github.com/processing/p5.js-compatibility)",
+    "CustomVersionTitle": "Managing your own libraries? Nice!",
     "CustomVersionInfo": "The version of p5.js is currently being managed in the code of index.html. This means it can't be adjusted from this tab.",
     "CustomVersionReset": "If you'd like to use the default libraries, you can replace the script tags in index.html with the following:",
     "SoundAddon": "p5.sound.js Addon",
@@ -666,8 +667,5 @@
   },
   "SkipLink": {
     "PlaySketch": "Skip to Play Sketch"
-  },
-  "Admonition": {
-    "Note": "Note"
   }
 }

From 261fd12857876d0a9a2b6fa2bfd566fb84d91cd9 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 22 Mar 2025 15:55:31 -0400
Subject: [PATCH 21/34] Update admonition colors for contrast

---
 client/styles/abstracts/_variables.scss | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss
index 8ad2e57062..361ad57a38 100644
--- a/client/styles/abstracts/_variables.scss
+++ b/client/styles/abstracts/_variables.scss
@@ -129,7 +129,7 @@ $themes: (
 
     admonition-border: #22C8ED,
     admonition-background: #E4F8FF,
-    admonition-text: #0E86A1,
+    admonition-text: #075769,
   ),
   dark: (
     logo-color: $p5js-pink,
@@ -227,8 +227,8 @@ $themes: (
     hint-inline-text-color: #cfcfcf,
 
     admonition-border: #22C8ED,
-    admonition-background: #1681b7,
-    admonition-text: #E4F8FF,
+    admonition-background: #105A7F,
+    admonition-text: #FFFFFF,
   ),
   contrast: (
     logo-color: $yellow,
@@ -326,7 +326,7 @@ $themes: (
     hint-inline-text-color: #cfcfcf,
 
     admonition-border: #22C8ED,
-    admonition-background: #1681b7,
+    admonition-background: #000000,
     admonition-text: #ffffff,
   )
 );

From cb255e2522f2b2f97f0fc3d4858aab01254d84cf Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 22 Mar 2025 15:55:42 -0400
Subject: [PATCH 22/34] Add better hover state for version picker

---
 client/modules/IDE/components/VersionIndicator.jsx | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index 317514022c..8a46167d80 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -1,11 +1,20 @@
 import React, { useCallback } from 'react';
+import styled from 'styled-components';
 import { useTranslation } from 'react-i18next';
 import { useDispatch } from 'react-redux';
 import { openPreferences } from '../actions/ide';
 import { setPreferencesTab } from '../actions/preferences';
+import { remSize, prop } from '../../../theme';
 
 import { useP5Version } from '../hooks/useP5Version';
 
+const VersionPickerButton = styled.button`
+  color: ${prop('Button.primary.default.foreground')};
+  &:hover {
+    color: ${prop('Button.primary.hover.background')} !important;
+  }
+`;
+
 const VersionIndicator = () => {
   const { versionInfo } = useP5Version();
   const { t } = useTranslation();
@@ -17,11 +26,11 @@ const VersionIndicator = () => {
   }, []);
 
   return (
-    <button onClick={openVersionSettings}>
+    <VersionPickerButton onClick={openVersionSettings}>
       {t('Toolbar.LibraryVersion')}
       &nbsp;
       {versionInfo?.version || t('Toolbar.CustomLibraryVersion')}
-    </button>
+    </VersionPickerButton>
   );
 };
 

From 9139d02474dfe33401af73b95ffb881f3c9db36a Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 22 Mar 2025 16:07:08 -0400
Subject: [PATCH 23/34] Add pencil icon next to p5 version

---
 client/images/pencil.svg                       |  2 +-
 .../IDE/components/VersionIndicator.jsx        | 18 +++++++++++++++++-
 translations/locales/en-US/translations.json   |  2 +-
 3 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/client/images/pencil.svg b/client/images/pencil.svg
index f9bd1db9b7..a2ac9f4391 100644
--- a/client/images/pencil.svg
+++ b/client/images/pencil.svg
@@ -4,7 +4,7 @@
 <svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 	 width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
 <g>
-	<path d="M68.201,42.768c0.641,1.84,0.48,3.76-0.399,4.96L41.241,74.289l-15.76,4.32c-0.24,0.08-0.32,0-0.24-0.24l4.48-15.601
+	<path fill="currentColor" d="M68.201,42.768c0.641,1.84,0.48,3.76-0.399,4.96L41.241,74.289l-15.76,4.32c-0.24,0.08-0.32,0-0.24-0.24l4.48-15.601
 		l26.561-26.561c1.2-0.96,3.12-1.04,4.96-0.32C32.121,65.009,32.041,65.089,32.041,65.089l-1.84,5.84c0,0,0.88,0.32,1.68,1.12
 		c0.8,0.801,1.04,1.76,1.04,1.76l6-1.84L68.201,42.768z M36.521,65.169l27.68-27.681c0.88,0.48,1.761,1.36,2.4,2.32
 		c-27.761,27.68-27.761,27.68-27.761,27.68l-2.24-0.08L36.521,65.169z M76.282,39.248l-5.12,5.2c0,0,2.479-3.84-2.721-8.88
diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index 8a46167d80..ed34aced3b 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -4,15 +4,26 @@ import { useTranslation } from 'react-i18next';
 import { useDispatch } from 'react-redux';
 import { openPreferences } from '../actions/ide';
 import { setPreferencesTab } from '../actions/preferences';
-import { remSize, prop } from '../../../theme';
+import { prop } from '../../../theme';
+import EditIcon from '../../../images/pencil.svg';
 
 import { useP5Version } from '../hooks/useP5Version';
 
 const VersionPickerButton = styled.button`
   color: ${prop('Button.primary.default.foreground')};
+
   &:hover {
     color: ${prop('Button.primary.hover.background')} !important;
   }
+
+  & svg {
+    vertical-align: middle;
+    margin-bottom: 2px;
+  }
+
+  &:hover path {
+    fill: currentColor !important;
+  }
 `;
 
 const VersionIndicator = () => {
@@ -30,6 +41,11 @@ const VersionIndicator = () => {
       {t('Toolbar.LibraryVersion')}
       &nbsp;
       {versionInfo?.version || t('Toolbar.CustomLibraryVersion')}
+      <EditIcon
+        className="editable-input__icon"
+        focusable="false"
+        aria-hidden="true"
+      />
     </VersionPickerButton>
   );
 };
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index a0ed805f27..44dd59f7a7 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -157,7 +157,7 @@
     "EditSketchARIA": "Edit sketch name",
     "NewSketchNameARIA": "New sketch name",
     "By": " by ",
-    "LibraryVersion": "p5.js Version:",
+    "LibraryVersion": "p5.js version:",
     "CustomLibraryVersion": "Custom"
   },
   "Console": {

From 22871b5b80c8cd87a0085a7aebaa2b7f1415904d Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 22 Mar 2025 16:22:05 -0400
Subject: [PATCH 24/34] Use fieldset and legend tags

---
 .../IDE/components/Preferences/index.jsx      | 48 +++++++++----------
 client/styles/components/_preferences.scss    |  3 ++
 2 files changed, 27 insertions(+), 24 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index daeddce97f..8966b58dc7 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -171,7 +171,7 @@ export default function Preferences() {
         <TabPanel>
           <div className="preference">
             <h4 className="preference__title">{t('Preferences.Theme')}</h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setTheme('light'))}
@@ -214,7 +214,7 @@ export default function Preferences() {
               >
                 {t('Preferences.HighContrastTheme')}
               </label>
-            </div>
+            </fieldset>
           </div>
           <div className="preference">
             <h4 className="preference__title">{t('Preferences.TextSize')}</h4>
@@ -266,7 +266,7 @@ export default function Preferences() {
           </div>
           <div className="preference">
             <h4 className="preference__title">{t('Preferences.Autosave')}</h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setAutosave(true))}
@@ -293,13 +293,13 @@ export default function Preferences() {
               <label htmlFor="autosave-off" className="preference__option">
                 {t('Preferences.Off')}
               </label>
-            </div>
+            </fieldset>
           </div>
           <div className="preference">
             <h4 className="preference__title">
               {t('Preferences.AutocloseBracketsQuotes')}
             </h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setAutocloseBracketsQuotes(true))}
@@ -332,13 +332,13 @@ export default function Preferences() {
               >
                 {t('Preferences.Off')}
               </label>
-            </div>
+            </fieldset>
           </div>
           <div className="preference">
             <h4 className="preference__title">
               {t('Preferences.AutocompleteHinter')}
             </h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setAutocompleteHinter(true))}
@@ -371,11 +371,11 @@ export default function Preferences() {
               >
                 {t('Preferences.Off')}
               </label>
-            </div>
+            </fieldset>
           </div>
           <div className="preference">
             <h4 className="preference__title">{t('Preferences.WordWrap')}</h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setLinewrap(true))}
@@ -402,7 +402,7 @@ export default function Preferences() {
               <label htmlFor="linewrap-off" className="preference__option">
                 {t('Preferences.Off')}
               </label>
-            </div>
+            </fieldset>
           </div>
         </TabPanel>
         <TabPanel>
@@ -410,7 +410,7 @@ export default function Preferences() {
             <h4 className="preference__title">
               {t('Preferences.LineNumbers')}
             </h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setLineNumbers(true))}
@@ -437,13 +437,13 @@ export default function Preferences() {
               <label htmlFor="line-numbers-off" className="preference__option">
                 {t('Preferences.Off')}
               </label>
-            </div>
+            </fieldset>
           </div>
           <div className="preference">
             <h4 className="preference__title">
               {t('Preferences.LintWarningSound')}
             </h4>
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="radio"
                 onChange={() => dispatch(setLintWarning(true))}
@@ -477,7 +477,7 @@ export default function Preferences() {
               >
                 {t('Preferences.PreviewSound')}
               </button>
-            </div>
+            </fieldset>
           </div>
           <div className="preference">
             <h4 className="preference__title">
@@ -487,7 +487,7 @@ export default function Preferences() {
               {t('Preferences.UsedScreenReader')}
             </h6>
 
-            <div className="preference__options">
+            <fieldset className="preference__options">
               <input
                 type="checkbox"
                 onChange={(event) => {
@@ -522,7 +522,7 @@ export default function Preferences() {
               >
                 {t('Preferences.TableText')}
               </label>
-            </div>
+            </fieldset>
           </div>
         </TabPanel>
         <TabPanel>
@@ -567,7 +567,7 @@ export default function Preferences() {
                 <h4 className="preference__title">
                   {t('Preferences.SoundAddon')}
                 </h4>
-                <div className="preference__options">
+                <fieldset className="preference__options">
                   <input
                     type="radio"
                     onChange={() => {
@@ -618,17 +618,17 @@ export default function Preferences() {
                     {t('Preferences.Off')}
                   </label>
                   {versionInfo.lastP5SoundURL && (
-                    <span className="preference__warning">
+                    <legend className="preference__warning">
                       {t('Preferences.UndoSoundVersion')}
-                    </span>
+                    </legend>
                   )}
-                </div>
+                </fieldset>
               </div>
               <div className="preference">
                 <h4 className="preference__title">
                   {t('Preferences.PreloadAddon')}
                 </h4>
-                <div className="preference__options">
+                <fieldset className="preference__options">
                   <input
                     type="radio"
                     onChange={() =>
@@ -665,13 +665,13 @@ export default function Preferences() {
                   >
                     {t('Preferences.Off')}
                   </label>
-                </div>
+                </fieldset>
               </div>
               <div className="preference">
                 <h4 className="preference__title">
                   {t('Preferences.ShapesAddon')}
                 </h4>
-                <div className="preference__options">
+                <fieldset className="preference__options">
                   <input
                     type="radio"
                     onChange={() =>
@@ -708,7 +708,7 @@ export default function Preferences() {
                   >
                     {t('Preferences.Off')}
                   </label>
-                </div>
+                </fieldset>
               </div>
             </>
           )}
diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss
index bbf2ce8979..e80bd83b1a 100644
--- a/client/styles/components/_preferences.scss
+++ b/client/styles/components/_preferences.scss
@@ -149,6 +149,7 @@
 
 .preference__warning {
   @include themify() {
+    display: contents;
     font-weight: bold;
     color: getThemifyVariable("preferences-warning-color");
   }
@@ -189,6 +190,8 @@ input[type="number"]::-webkit-outer-spin-button {
 }
 
 .preference__options {
+  border: 0;
+  padding: 0;
   display: flex;
   justify-content: center;
   align-items: center;

From 3f9c28b8c3b48c226aaffa57342d5f317615db4d Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 22 Mar 2025 16:52:29 -0400
Subject: [PATCH 25/34] Use fieldsets, fix aria labels

---
 .../IDE/components/Preferences/index.jsx      | 69 ++++++++++++++++---
 client/modules/IDE/hooks/useP5Version.jsx     | 28 ++++++--
 translations/locales/en-US/translations.json  |  7 +-
 3 files changed, 90 insertions(+), 14 deletions(-)

diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx
index 8966b58dc7..bc5d353e85 100644
--- a/client/modules/IDE/components/Preferences/index.jsx
+++ b/client/modules/IDE/components/Preferences/index.jsx
@@ -583,7 +583,9 @@ export default function Preferences() {
                         updateHTML(versionInfo.setP5Sound(true));
                       }
                     }}
-                    aria-label={t('Preferences.AutosaveOnARIA')}
+                    aria-label={`${t('Preferences.SoundAddon')} ${t(
+                      'Preferences.AddonOn'
+                    )}`}
                     name="soundaddon"
                     id="soundaddon-on"
                     className="preference__radio-button"
@@ -604,7 +606,9 @@ export default function Preferences() {
                       }
                       updateHTML(versionInfo.setP5Sound(false));
                     }}
-                    aria-label={t('Preferences.AutosaveOffARIA')}
+                    aria-label={`${t('Preferences.SoundAddon')} ${t(
+                      'Preferences.AddonOff'
+                    )}`}
                     name="soundaddon"
                     id="soundaddon-off"
                     className="preference__radio-button"
@@ -634,7 +638,9 @@ export default function Preferences() {
                     onChange={() =>
                       updateHTML(versionInfo.setP5PreloadAddon(true))
                     }
-                    aria-label={t('Preferences.AutosaveOnARIA')}
+                    aria-label={`${t('Preferences.PreloadAddon')} ${t(
+                      'Preferences.AddonOn'
+                    )}`}
                     name="preloadaddon"
                     id="preloadaddon-on"
                     className="preference__radio-button"
@@ -652,7 +658,9 @@ export default function Preferences() {
                     onChange={() =>
                       updateHTML(versionInfo.setP5PreloadAddon(false))
                     }
-                    aria-label={t('Preferences.AutosaveOffARIA')}
+                    aria-label={`${t('Preferences.PreloadAddon')} ${t(
+                      'Preferences.AddonOff'
+                    )}`}
                     name="preloadaddon"
                     id="preloadaddon-off"
                     className="preference__radio-button"
@@ -677,12 +685,14 @@ export default function Preferences() {
                     onChange={() =>
                       updateHTML(versionInfo.setP5ShapesAddon(true))
                     }
-                    aria-label={t('Preferences.AutosaveOnARIA')}
+                    aria-label={`${t('Preferences.ShapesAddon')} ${t(
+                      'Preferences.AddonOn'
+                    )}`}
                     name="shapesaddon"
                     id="shapesaddon-on"
                     className="preference__radio-button"
                     value="On"
-                    checked={versionInfo.p5ShapesAdddon}
+                    checked={versionInfo.p5ShapesAddon}
                   />
                   <label
                     htmlFor="shapesaddon-on"
@@ -695,12 +705,14 @@ export default function Preferences() {
                     onChange={() =>
                       updateHTML(versionInfo.setP5ShapesAddon(false))
                     }
-                    aria-label={t('Preferences.AutosaveOffARIA')}
+                    aria-label={`${t('Preferences.ShapesAddon')} ${t(
+                      'Preferences.AddonOff'
+                    )}`}
                     name="shapesaddon"
                     id="shapesaddon-off"
                     className="preference__radio-button"
                     value="Off"
-                    checked={!versionInfo.p5ShapesAdddon}
+                    checked={!versionInfo.p5ShapesAddon}
                   />
                   <label
                     htmlFor="shapesaddon-off"
@@ -710,6 +722,47 @@ export default function Preferences() {
                   </label>
                 </fieldset>
               </div>
+              <div className="preference">
+                <h4 className="preference__title">
+                  {t('Preferences.DataAddon')}
+                </h4>
+                <fieldset className="preference__options">
+                  <input
+                    type="radio"
+                    onChange={() =>
+                      updateHTML(versionInfo.setP5DataAddon(true))
+                    }
+                    aria-label={`${t('Preferences.DataAddon')} ${t(
+                      'Preferences.AddonOn'
+                    )}`}
+                    name="dataaddon"
+                    id="dataaddon-on"
+                    className="preference__radio-button"
+                    value="On"
+                    checked={versionInfo.p5DataAddon}
+                  />
+                  <label htmlFor="dataaddon-on" className="preference__option">
+                    {t('Preferences.On')}
+                  </label>
+                  <input
+                    type="radio"
+                    onChange={() =>
+                      updateHTML(versionInfo.setP5DataAddon(false))
+                    }
+                    aria-label={`${t('Preferences.DataAddon')} ${t(
+                      'Preferences.AddonOff'
+                    )}`}
+                    name="dataaddon"
+                    id="dataaddon-off"
+                    className="preference__radio-button"
+                    value="Off"
+                    checked={!versionInfo.p5DataAddon}
+                  />
+                  <label htmlFor="dataaddon-off" className="preference__option">
+                    {t('Preferences.Off')}
+                  </label>
+                </fieldset>
+              </div>
             </>
           )}
         </TabPanel>
diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index 29870ae160..00a8695675 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -136,8 +136,12 @@ export const p5Versions = [
 export const currentP5Version = p5Versions[0];
 
 export const p5SoundURL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/addons/p5.sound.min.js`;
-export const p5PreloadAddonURL = 'https://TODO/preload.js';
-export const p5ShapesAddonURL = 'https://TODO/shapes.js';
+export const p5PreloadAddonURL =
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.0.1/src/preload.js';
+export const p5ShapesAddonURL =
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.0.1/src/shapes.js';
+export const p5DataAddonURL =
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.0.1/src/data.js';
 export const p5URL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/p5.js`;
 
 const P5VersionContext = React.createContext({});
@@ -259,6 +263,20 @@ export function P5VersionProvider(props) {
         return serializeResult();
       };
 
+      const p5DataAddonNode = [
+        ...dom.documentElement.querySelectorAll('script')
+      ].find((s) => s.getAttribute('src') === p5DataAddonURL);
+      const setP5DataAddon = function (enabled) {
+        if (!enabled && p5DataAddonNode) {
+          p5DataAddonNode.parentNode.removeChild(p5DataAddonNode);
+        } else if (enabled && !p5DataAddonNode) {
+          const newNode = document.createElement('script');
+          newNode.setAttribute('src', p5DataAddonURL);
+          scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
+        }
+        return serializeResult();
+      };
+
       return {
         version,
         minified,
@@ -271,8 +289,10 @@ export function P5VersionProvider(props) {
         setLastP5SoundURL,
         p5PreloadAddon: !!p5PreloadAddonNode,
         setP5PreloadAddon,
-        p5ShapesAdddon: !!p5ShapesAddonNode,
-        setP5ShapesAddon
+        p5ShapesAddon: !!p5ShapesAddonNode,
+        setP5ShapesAddon,
+        p5DataAddon: !!p5DataAddonNode,
+        setP5DataAddon
       };
     }
     return null;
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 44dd59f7a7..62221541bf 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -222,8 +222,11 @@
     "CustomVersionInfo": "The version of p5.js is currently being managed in the code of index.html. This means it can't be adjusted from this tab.",
     "CustomVersionReset": "If you'd like to use the default libraries, you can replace the script tags in index.html with the following:",
     "SoundAddon": "p5.sound.js Addon",
-    "PreloadAddon": "p5.js 2.0 Addon - Preload",
-    "ShapesAddon": "p5.js 2.0 Addon - Shapes",
+    "PreloadAddon": "p5.js 1.x Compatibility Add-on — Preload",
+    "ShapesAddon": "p5.js 1.x Compatibility Add-on — Shapes",
+    "DataAddon": "p5.js 1.x Compatibility Add-on — Data Structures",
+    "AddonOnARIA": "on",
+    "AddonOffARIA": "off",
     "UndoSoundVersion": "Want to use p5.sound.js again? Turning it back on will restore the version you were using before.",
     "CopyToClipboardSuccess": "Copied to clipboard!",
     "CopyToClipboardFailure": "We weren't able to copy the text, try selecting it and copying it manually."

From 9d2444fb2186595f214891c4f3671e80ba429a31 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 22 Mar 2025 17:02:12 -0400
Subject: [PATCH 26/34] Use gear icon

---
 client/modules/IDE/components/VersionIndicator.jsx | 11 +++++------
 client/modules/IDE/components/VersionPicker.jsx    |  2 +-
 translations/locales/en-US/translations.json       |  6 +++---
 3 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index ed34aced3b..9d14931d09 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
 import { openPreferences } from '../actions/ide';
 import { setPreferencesTab } from '../actions/preferences';
 import { prop } from '../../../theme';
-import EditIcon from '../../../images/pencil.svg';
+import EditIcon from '../../../images/preferences.svg';
 
 import { useP5Version } from '../hooks/useP5Version';
 
@@ -19,6 +19,9 @@ const VersionPickerButton = styled.button`
   & svg {
     vertical-align: middle;
     margin-bottom: 2px;
+    margin-left: 0.5rem;
+    width: 1rem;
+    height: 1rem;
   }
 
   &:hover path {
@@ -41,11 +44,7 @@ const VersionIndicator = () => {
       {t('Toolbar.LibraryVersion')}
       &nbsp;
       {versionInfo?.version || t('Toolbar.CustomLibraryVersion')}
-      <EditIcon
-        className="editable-input__icon"
-        focusable="false"
-        aria-hidden="true"
-      />
+      <EditIcon focusable="false" aria-hidden="true" />
     </VersionPickerButton>
   );
 };
diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx
index bb59382ad7..a42743d353 100644
--- a/client/modules/IDE/components/VersionPicker.jsx
+++ b/client/modules/IDE/components/VersionPicker.jsx
@@ -38,7 +38,7 @@ const VersionDropdownMenu = styled(DropdownMenu)`
     padding: 0;
   }
 
-  margin-bottom: 1rem;
+  margin-bottom: 0.5rem;
 `;
 
 const VersionPicker = React.forwardRef(({ onChangeVersion }, ref) => {
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 62221541bf..b7d4f7533d 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -222,9 +222,9 @@
     "CustomVersionInfo": "The version of p5.js is currently being managed in the code of index.html. This means it can't be adjusted from this tab.",
     "CustomVersionReset": "If you'd like to use the default libraries, you can replace the script tags in index.html with the following:",
     "SoundAddon": "p5.sound.js Addon",
-    "PreloadAddon": "p5.js 1.x Compatibility Add-on — Preload",
-    "ShapesAddon": "p5.js 1.x Compatibility Add-on — Shapes",
-    "DataAddon": "p5.js 1.x Compatibility Add-on — Data Structures",
+    "PreloadAddon": "p5.js 1.x Compatibility Addon — Preload",
+    "ShapesAddon": "p5.js 1.x Compatibility Addon — Shapes",
+    "DataAddon": "p5.js 1.x Compatibility Addon — Data Structures",
     "AddonOnARIA": "on",
     "AddonOffARIA": "off",
     "UndoSoundVersion": "Want to use p5.sound.js again? Turning it back on will restore the version you were using before.",

From 3349904c96c8ca20e6285d47fe4f5d8f5e58a210 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Tue, 25 Mar 2025 16:02:36 -0400
Subject: [PATCH 27/34] Use new wording for addons

---
 translations/locales/en-US/translations.json | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index b7d4f7533d..2c342737a4 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -221,10 +221,10 @@
     "CustomVersionTitle": "Managing your own libraries? Nice!",
     "CustomVersionInfo": "The version of p5.js is currently being managed in the code of index.html. This means it can't be adjusted from this tab.",
     "CustomVersionReset": "If you'd like to use the default libraries, you can replace the script tags in index.html with the following:",
-    "SoundAddon": "p5.sound.js Addon",
-    "PreloadAddon": "p5.js 1.x Compatibility Addon — Preload",
-    "ShapesAddon": "p5.js 1.x Compatibility Addon — Shapes",
-    "DataAddon": "p5.js 1.x Compatibility Addon — Data Structures",
+    "SoundAddon": "p5.sound.js Add-on Library",
+    "PreloadAddon": "p5.js 1.x Compatibility Add-on Library — Preload",
+    "ShapesAddon": "p5.js 1.x Compatibility Add-on Library — Shapes",
+    "DataAddon": "p5.js 1.x Compatibility Add-on Library — Data Structures",
     "AddonOnARIA": "on",
     "AddonOffARIA": "off",
     "UndoSoundVersion": "Want to use p5.sound.js again? Turning it back on will restore the version you were using before.",

From 2b2fb956b99c7718ef3582a3d133b7d1782f38b7 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Tue, 25 Mar 2025 16:20:45 -0400
Subject: [PATCH 28/34] Add notification dot with label

---
 .../IDE/components/VersionIndicator.jsx       | 40 ++++++++++++++++---
 client/theme.js                               |  3 ++
 translations/locales/en-US/translations.json  |  5 ++-
 3 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index 9d14931d09..34f987c97a 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import styled from 'styled-components';
 import { useTranslation } from 'react-i18next';
 import { useDispatch } from 'react-redux';
@@ -29,21 +29,51 @@ const VersionPickerButton = styled.button`
   }
 `;
 
+const NotificationDot = styled.div`
+  display: inline-block;
+  vertical-align: top;
+  border-radius: 50%;
+  width: 0.7rem;
+  height: 0.7rem;
+  background-color: ${prop('colors.dodgerblue')};
+  margin-left: 0.25rem;
+`;
+
+const CLICKED_LIBRARY_VERSION_KEY = 'clickedLibraryVersionIndicator';
+
 const VersionIndicator = () => {
   const { versionInfo } = useP5Version();
   const { t } = useTranslation();
   const dispatch = useDispatch();
+  const [showNotificationDot, setShowNotificationDot] = useState(false);
+
+  useEffect(() => {
+    const hasHiddenDot = window.localStorage.getItem(
+      CLICKED_LIBRARY_VERSION_KEY
+    );
+    setShowNotificationDot(!hasHiddenDot);
+  }, []);
 
   const openVersionSettings = useCallback(() => {
     dispatch(openPreferences());
     dispatch(setPreferencesTab(2));
+    setShowNotificationDot(false);
+    window.localStorage.setItem(CLICKED_LIBRARY_VERSION_KEY, true);
   }, []);
 
+  const label = t('Toolbar.LibraryVersion');
+  const currentVersion =
+    versionInfo?.version || t('Toolbar.CustomLibraryVersion');
+  let ariaLabel = `${label}: ${currentVersion}`;
+  if (showNotificationDot) {
+    ariaLabel = `${t('Toolbar.Notification')} - ${ariaLabel}`;
+  }
+
   return (
-    <VersionPickerButton onClick={openVersionSettings}>
-      {t('Toolbar.LibraryVersion')}
-      &nbsp;
-      {versionInfo?.version || t('Toolbar.CustomLibraryVersion')}
+    <VersionPickerButton onClick={openVersionSettings} ariaLabel={ariaLabel}>
+      {label}:&nbsp;
+      {currentVersion}
+      {showNotificationDot && <NotificationDot />}
       <EditIcon focusable="false" aria-hidden="true" />
     </VersionPickerButton>
   );
diff --git a/client/theme.js b/client/theme.js
index f7e907ec5c..d26e971c49 100644
--- a/client/theme.js
+++ b/client/theme.js
@@ -79,6 +79,7 @@ const baseThemes = {
     modalBorderColor: grays.middleLight,
     searchBackgroundColor: grays.lightest,
     tableRowStripeColor: grays.mediumLight,
+    notification: colors.dodgerblue,
 
     Button: {
       primary: {
@@ -170,6 +171,7 @@ const baseThemes = {
     modalBorderColor: grays.middleDark,
     searchBackgroundColor: grays.darker,
     tableRowStripeColor: grays.dark,
+    notification: colors.processingBlueLight,
 
     Button: {
       primary: {
@@ -257,6 +259,7 @@ export default {
   [Theme.contrast]: extend(baseThemes[Theme.dark], {
     inactiveTextColor: grays.light,
     logoColor: colors.yellow,
+    notification: colors.p5ContrastYellow,
 
     Button: {
       primary: {
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 2c342737a4..3d2881056e 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -157,8 +157,9 @@
     "EditSketchARIA": "Edit sketch name",
     "NewSketchNameARIA": "New sketch name",
     "By": " by ",
-    "LibraryVersion": "p5.js version:",
-    "CustomLibraryVersion": "Custom"
+    "LibraryVersion": "p5.js version",
+    "CustomLibraryVersion": "Custom",
+    "Notification": "New"
   },
   "Console": {
     "Title": "Console",

From 0d9430a49ed5d059b3629cf380715db17b07cac7 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Tue, 25 Mar 2025 16:24:51 -0400
Subject: [PATCH 29/34] Update aria format

---
 client/modules/IDE/components/VersionIndicator.jsx | 10 ++++++----
 translations/locales/en-US/translations.json       |  3 ++-
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index 34f987c97a..18131e8e21 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -64,10 +64,12 @@ const VersionIndicator = () => {
   const label = t('Toolbar.LibraryVersion');
   const currentVersion =
     versionInfo?.version || t('Toolbar.CustomLibraryVersion');
-  let ariaLabel = `${label}: ${currentVersion}`;
-  if (showNotificationDot) {
-    ariaLabel = `${t('Toolbar.Notification')} - ${ariaLabel}`;
-  }
+  const description = t(
+    showNotificationDot
+      ? 'Toolbar.NewVersionPickerARIA'
+      : 'Toolbar.VersionPickerARIA'
+  );
+  const ariaLabel = `${label}: ${currentVersion} - ${description}`;
 
   return (
     <VersionPickerButton onClick={openVersionSettings} ariaLabel={ariaLabel}>
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index 3d2881056e..d2b8b5bef6 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -159,7 +159,8 @@
     "By": " by ",
     "LibraryVersion": "p5.js version",
     "CustomLibraryVersion": "Custom",
-    "Notification": "New"
+    "VersionPickerARIA": "Version picker",
+    "NewVersionPickerARIA": "Version picker"
   },
   "Console": {
     "Title": "Console",

From d7088fb31544521f445c9cff4a8ed794ccb6a0ac Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Fri, 28 Mar 2025 15:29:45 -0400
Subject: [PATCH 30/34] Move version indicator to the toolbar

---
 .../modules/IDE/components/Editor/index.jsx   |  3 ---
 .../modules/IDE/components/Header/Toolbar.jsx |  3 +++
 .../IDE/components/VersionIndicator.jsx       | 19 +++++++++----------
 client/styles/components/_toolbar.scss        |  1 -
 translations/locales/en-US/translations.json  |  3 +--
 5 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index fce05e261f..2daa186594 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -576,9 +576,6 @@ class Editor extends React.Component {
                   </span>
                   <Timer />
                 </div>
-                <div className="editor__library-version">
-                  <VersionIndicator />
-                </div>
               </div>
               <article
                 ref={(element) => {
diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx
index 5ebdbdafea..9f21096176 100644
--- a/client/modules/IDE/components/Header/Toolbar.jsx
+++ b/client/modules/IDE/components/Header/Toolbar.jsx
@@ -20,6 +20,7 @@ import PlayIcon from '../../../../images/play.svg';
 import StopIcon from '../../../../images/stop.svg';
 import PreferencesIcon from '../../../../images/preferences.svg';
 import ProjectName from './ProjectName';
+import VersionIndicator from '../VersionIndicator';
 
 const Toolbar = (props) => {
   const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector(
@@ -113,6 +114,8 @@ const Toolbar = (props) => {
           return null;
         })()}
       </div>
+      <div style={{ flex: 1 }} />
+      <VersionIndicator />
       <button
         className={preferencesButtonClass}
         onClick={() => dispatch(openPreferences())}
diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index 18131e8e21..d122ab3a0a 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -5,12 +5,13 @@ import { useDispatch } from 'react-redux';
 import { openPreferences } from '../actions/ide';
 import { setPreferencesTab } from '../actions/preferences';
 import { prop } from '../../../theme';
-import EditIcon from '../../../images/preferences.svg';
+import EditIcon from '../../../images/pencil.svg';
 
 import { useP5Version } from '../hooks/useP5Version';
 
 const VersionPickerButton = styled.button`
   color: ${prop('Button.primary.default.foreground')};
+  margin-right: 1rem;
 
   &:hover {
     color: ${prop('Button.primary.hover.background')} !important;
@@ -19,9 +20,8 @@ const VersionPickerButton = styled.button`
   & svg {
     vertical-align: middle;
     margin-bottom: 2px;
-    margin-left: 0.5rem;
-    width: 1rem;
-    height: 1rem;
+    width: 1.5rem;
+    height: 1.5rem;
   }
 
   &:hover path {
@@ -61,22 +61,21 @@ const VersionIndicator = () => {
     window.localStorage.setItem(CLICKED_LIBRARY_VERSION_KEY, true);
   }, []);
 
-  const label = t('Toolbar.LibraryVersion');
-  const currentVersion =
-    versionInfo?.version || t('Toolbar.CustomLibraryVersion');
+  const currentVersion = versionInfo?.version
+    ? `p5.js ${versionInfo.version}`
+    : t('Toolbar.CustomLibraryVersion');
   const description = t(
     showNotificationDot
       ? 'Toolbar.NewVersionPickerARIA'
       : 'Toolbar.VersionPickerARIA'
   );
-  const ariaLabel = `${label}: ${currentVersion} - ${description}`;
+  const ariaLabel = `${currentVersion} - ${description}`;
 
   return (
     <VersionPickerButton onClick={openVersionSettings} ariaLabel={ariaLabel}>
-      {label}:&nbsp;
       {currentVersion}
-      {showNotificationDot && <NotificationDot />}
       <EditIcon focusable="false" aria-hidden="true" />
+      {showNotificationDot && <NotificationDot />}
     </VersionPickerButton>
   );
 };
diff --git a/client/styles/components/_toolbar.scss b/client/styles/components/_toolbar.scss
index 00f2be6ec3..dcb344fd9c 100644
--- a/client/styles/components/_toolbar.scss
+++ b/client/styles/components/_toolbar.scss
@@ -70,7 +70,6 @@
 			@extend %toolbar-button--selected;
 		}
 	}
-	margin-left: auto;
 	& span {
 		padding-left: #{math.div(1, $base-font-size)}rem;
 		display: flex;
diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json
index d2b8b5bef6..c7eb693cbc 100644
--- a/translations/locales/en-US/translations.json
+++ b/translations/locales/en-US/translations.json
@@ -157,8 +157,7 @@
     "EditSketchARIA": "Edit sketch name",
     "NewSketchNameARIA": "New sketch name",
     "By": " by ",
-    "LibraryVersion": "p5.js version",
-    "CustomLibraryVersion": "Custom",
+    "CustomLibraryVersion": "Custom p5.js version",
     "VersionPickerARIA": "Version picker",
     "NewVersionPickerARIA": "Version picker"
   },

From 3ac6c95d175df19d48981368ba03ff96ec753d26 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Fri, 4 Apr 2025 08:29:27 -0400
Subject: [PATCH 31/34] Use new compatibility addon URL, support new p5.sound
 for 2.0, preemptively add 2.0 release and remove betas

---
 client/modules/IDE/hooks/useP5Version.jsx | 24 ++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index 00a8695675..8eac384a93 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -7,8 +7,8 @@ import PropTypes from 'prop-types';
 // JSON.stringify([...document.querySelectorAll('._132722c7')].map(n => n.innerText), null, 2)
 // TODO: use their API for this to grab these at build time?
 export const p5Versions = [
+  '2.0.0',
   '1.11.3',
-  '2.0.0-beta.2',
   '1.11.2',
   '1.11.1',
   '1.11.0',
@@ -133,15 +133,17 @@ export const p5Versions = [
   '0.2.1'
 ];
 
-export const currentP5Version = p5Versions[0];
+export const currentP5Version = '1.11.3'; // Don't update to 2.x until 2026
 
-export const p5SoundURL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/addons/p5.sound.min.js`;
+export const p5SoundURLOld = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/addons/p5.sound.min.js`;
+export const p5SoundURL =
+  'https://cdn.jsdelivr.net/npm/p5.sound@0.2.0/dist/p5.sound.min.js';
 export const p5PreloadAddonURL =
-  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.0.1/src/preload.js';
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.1/src/preload.js';
 export const p5ShapesAddonURL =
-  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.0.1/src/shapes.js';
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.1/src/shapes.js';
 export const p5DataAddonURL =
-  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.0.1/src/data.js';
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.1/src/data.js';
 export const p5URL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/p5.js`;
 
 const P5VersionContext = React.createContext({});
@@ -200,7 +202,7 @@ export function P5VersionProvider(props) {
         const file = minified ? 'p5.min.js' : 'p5.js';
         scriptNode.setAttribute(
           'src',
-          `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${newVersion}/${file}`
+          `https://cdn.jsdelivr.net/npm/p5@${newVersion}/lib/${file}`
         );
         return serializeResult();
       };
@@ -210,7 +212,8 @@ export function P5VersionProvider(props) {
       ].find((s) =>
         [
           /^https?:\/\/cdnjs.cloudflare.com\/ajax\/libs\/p5.js\/(.+)\/addons\/p5\.sound(?:\.min)?\.js$/,
-          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5@(.+)\/lib\/addons\/p5\.sound(?:\.min)?\.js$/
+          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5@(.+)\/lib\/addons\/p5\.sound(?:\.min)?\.js$/,
+          /^https?:\/\/cdn.jsdelivr.net\/npm\/p5.sound@(.+)\/dist\/p5\.sound(?:\.min)?\.js$/
         ].some((regex) => regex.exec(s.getAttribute('src') || ''))
       );
       const setP5Sound = function (enabled) {
@@ -218,7 +221,10 @@ export function P5VersionProvider(props) {
           p5SoundNode.parentNode.removeChild(p5SoundNode);
         } else if (enabled && !p5SoundNode) {
           const newNode = document.createElement('script');
-          newNode.setAttribute('src', p5SoundURL);
+          newNode.setAttribute(
+            'src',
+            version.startsWith('2') ? p5SoundURL : p5SoundURLOld
+          );
           scriptNode.parentNode.insertBefore(newNode, scriptNode.nextSibling);
         }
         return serializeResult();

From 55ab78e77844eda489aa7baa3b9af6394f000753 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Sat, 5 Apr 2025 10:08:09 -0400
Subject: [PATCH 32/34] Move indicator to the left, switch back to gear icon

---
 client/modules/IDE/components/Editor/index.jsx     | 1 -
 client/modules/IDE/components/Header/Toolbar.jsx   | 2 +-
 client/modules/IDE/components/VersionIndicator.jsx | 9 +++++----
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx
index 2daa186594..90a0b450fb 100644
--- a/client/modules/IDE/components/Editor/index.jsx
+++ b/client/modules/IDE/components/Editor/index.jsx
@@ -72,7 +72,6 @@ import UnsavedChangesIndicator from '../UnsavedChangesIndicator';
 import { EditorContainer, EditorHolder } from './MobileEditor';
 import { FolderIcon } from '../../../../common/icons';
 import IconButton from '../../../../common/IconButton';
-import VersionIndicator from '../VersionIndicator';
 
 emmet(CodeMirror);
 
diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx
index 9f21096176..dd6e175d36 100644
--- a/client/modules/IDE/components/Header/Toolbar.jsx
+++ b/client/modules/IDE/components/Header/Toolbar.jsx
@@ -114,8 +114,8 @@ const Toolbar = (props) => {
           return null;
         })()}
       </div>
-      <div style={{ flex: 1 }} />
       <VersionIndicator />
+      <div style={{ flex: 1 }} />
       <button
         className={preferencesButtonClass}
         onClick={() => dispatch(openPreferences())}
diff --git a/client/modules/IDE/components/VersionIndicator.jsx b/client/modules/IDE/components/VersionIndicator.jsx
index d122ab3a0a..0ec022927b 100644
--- a/client/modules/IDE/components/VersionIndicator.jsx
+++ b/client/modules/IDE/components/VersionIndicator.jsx
@@ -5,13 +5,13 @@ import { useDispatch } from 'react-redux';
 import { openPreferences } from '../actions/ide';
 import { setPreferencesTab } from '../actions/preferences';
 import { prop } from '../../../theme';
-import EditIcon from '../../../images/pencil.svg';
+import EditIcon from '../../../images/preferences.svg';
 
 import { useP5Version } from '../hooks/useP5Version';
 
 const VersionPickerButton = styled.button`
   color: ${prop('Button.primary.default.foreground')};
-  margin-right: 1rem;
+  margin-left: 1rem;
 
   &:hover {
     color: ${prop('Button.primary.hover.background')} !important;
@@ -20,8 +20,9 @@ const VersionPickerButton = styled.button`
   & svg {
     vertical-align: middle;
     margin-bottom: 2px;
-    width: 1.5rem;
-    height: 1.5rem;
+    width: 1rem;
+    height: 1rem;
+    margin-left: 0.25rem;
   }
 
   &:hover path {

From 93cb71505eaa7f049ee829ffa872f4df2074dd5d Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Tue, 15 Apr 2025 19:14:38 -0400
Subject: [PATCH 33/34] Bump compatibility addon version

---
 client/modules/IDE/hooks/useP5Version.jsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index 8eac384a93..3878698e39 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -139,11 +139,11 @@ export const p5SoundURLOld = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.
 export const p5SoundURL =
   'https://cdn.jsdelivr.net/npm/p5.sound@0.2.0/dist/p5.sound.min.js';
 export const p5PreloadAddonURL =
-  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.1/src/preload.js';
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.2/src/preload.js';
 export const p5ShapesAddonURL =
-  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.1/src/shapes.js';
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.2/src/shapes.js';
 export const p5DataAddonURL =
-  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.1/src/data.js';
+  'https://cdn.jsdelivr.net/npm/p5.js-compatibility@0.1.2/src/data.js';
 export const p5URL = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/${currentP5Version}/p5.js`;
 
 const P5VersionContext = React.createContext({});

From 312db23b4bc8f37b2e90244e2ab88801e7281921 Mon Sep 17 00:00:00 2001
From: Dave Pagurek <davepagurek@gmail.com>
Date: Wed, 16 Apr 2025 11:38:40 -0400
Subject: [PATCH 34/34] Update 1.x version

---
 client/modules/IDE/hooks/useP5Version.jsx   | 3 ++-
 server/domain-objects/createDefaultFiles.js | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx
index 3878698e39..4658d7aece 100644
--- a/client/modules/IDE/hooks/useP5Version.jsx
+++ b/client/modules/IDE/hooks/useP5Version.jsx
@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
 // TODO: use their API for this to grab these at build time?
 export const p5Versions = [
   '2.0.0',
+  '1.11.4',
   '1.11.3',
   '1.11.2',
   '1.11.1',
@@ -133,7 +134,7 @@ export const p5Versions = [
   '0.2.1'
 ];
 
-export const currentP5Version = '1.11.3'; // Don't update to 2.x until 2026
+export const currentP5Version = '1.11.4'; // Don't update to 2.x until 2026
 
 export const p5SoundURLOld = `https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/addons/p5.sound.min.js`;
 export const p5SoundURL =
diff --git a/server/domain-objects/createDefaultFiles.js b/server/domain-objects/createDefaultFiles.js
index 82a1470123..b2584983fc 100644
--- a/server/domain-objects/createDefaultFiles.js
+++ b/server/domain-objects/createDefaultFiles.js
@@ -9,8 +9,8 @@ function draw() {
 export const defaultHTML = `<!DOCTYPE html>
 <html lang="en">
   <head>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.4/p5.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.4/addons/p5.sound.min.js"></script>
     <link rel="stylesheet" type="text/css" href="style.css">
     <meta charset="utf-8" />