diff --git a/.github/PULL_REQUEST_TEMPLATE/generic.md b/.github/PULL_REQUEST_TEMPLATE/generic.md
index 32c54642cf..559b27fc56 100644
--- a/.github/PULL_REQUEST_TEMPLATE/generic.md
+++ b/.github/PULL_REQUEST_TEMPLATE/generic.md
@@ -15,6 +15,17 @@ about: Submit changes to the project for review and inclusion
This PR fixes #
+## PR Category
+
+
+
+
+- [ ] 🐛 Bug Fix — Fixes a bug or incorrect behavior
+- [ ] ✨ Feature — Adds new functionality
+- [ ] ⚡ Performance — Improves performance (load time, memory, rendering, etc.)
+- [ ] 🧪 Tests — Adds or updates test coverage
+- [ ] 📝 Documentation — Updates to docs, comments, or README
+
## Changes Made
diff --git a/.github/workflows/pr-category-check.yml b/.github/workflows/pr-category-check.yml
new file mode 100644
index 0000000000..804f5760c4
--- /dev/null
+++ b/.github/workflows/pr-category-check.yml
@@ -0,0 +1,113 @@
+name: PR Category Check
+
+# Ensures every PR has at least one category checkbox checked.
+# Automatically applies matching GitHub labels to the PR.
+# Uses pull_request_target so it works for fork PRs too (runs in base repo context).
+# This is safe because we only read the PR body from the event payload — no fork code is checked out or executed.
+
+on:
+ pull_request_target:
+ types: [opened, edited, synchronize, reopened]
+
+permissions:
+ pull-requests: write
+ contents: read
+
+jobs:
+ check-pr-category:
+ name: Validate PR Category & Auto-Label
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check categories and apply labels
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prBody = context.payload.pull_request.body || '';
+ const prNumber = context.payload.pull_request.number;
+
+ // Category checkboxes mapped to their GitHub label names
+ const categories = [
+ { label: 'Issue-Bug', emoji: '🐛', displayName: '🐛 Bug Fix', pattern: /- \[x\]\s*🐛\s*Bug Fix/i },
+ { label: 'Issue-Enhancement', emoji: '✨', displayName: '✨ Feature', pattern: /- \[x\]\s*✨\s*Feature/i },
+ { label: 'Issue-Performance', emoji: '⚡', displayName: '⚡ Performance', pattern: /- \[x\]\s*⚡\s*Performance/i },
+ { label: 'Issue-Testing', emoji: '🧪', displayName: '🧪 Tests', pattern: /- \[x\]\s*🧪\s*Tests/i },
+ { label: 'Issue-Documentation', emoji: '📝', displayName: '📝 Documentation', pattern: /- \[x\]\s*📝\s*Documentation/i },
+ ];
+
+ const checkedCategories = categories.filter(cat => cat.pattern.test(prBody));
+ const uncheckedCategories = categories.filter(cat => !cat.pattern.test(prBody));
+
+ // --- Step 1: Fail CI if no category is selected ---
+ if (checkedCategories.length === 0) {
+ const message = [
+ '## ❌ PR Category Required',
+ '',
+ 'This pull request does not have any **PR Category** selected.',
+ 'Please edit your PR description and check **at least one** category checkbox:',
+ '',
+ '| Category | Description |',
+ '|----------|-------------|',
+ '| 🐛 Bug Fix | Fixes a bug or incorrect behavior |',
+ '| ✨ Feature | Adds new functionality |',
+ '| ⚡ Performance | Improves performance |',
+ '| 🧪 Tests | Adds or updates test coverage |',
+ '| 📝 Documentation | Updates to docs, comments, or README |',
+ '',
+ 'Example: Change `- [ ] 🐛 Bug Fix` to `- [x] 🐛 Bug Fix`',
+ '',
+ '> **Tip:** You can select multiple categories if your PR spans several areas.',
+ ].join('\n');
+
+ core.setFailed(message);
+ return;
+ }
+
+ // --- Step 2: Auto-apply labels for checked categories ---
+ const labelsToAdd = checkedCategories.map(cat => cat.label);
+ const labelsToRemove = uncheckedCategories.map(cat => cat.label);
+
+ // Get current labels on the PR
+ const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ });
+ const currentLabelNames = currentLabels.map(l => l.name);
+
+ // Add labels that are checked but not yet on the PR
+ for (const label of labelsToAdd) {
+ if (!currentLabelNames.includes(label)) {
+ try {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ labels: [label],
+ });
+ core.info(`🏷️ Added label: "${label}"`);
+ } catch (error) {
+ core.warning(`⚠️ Could not add label "${label}". Make sure it exists in the repo. Error: ${error.message}`);
+ }
+ }
+ }
+
+ // Remove labels that are unchecked but still on the PR
+ for (const label of labelsToRemove) {
+ if (currentLabelNames.includes(label)) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ name: label,
+ });
+ core.info(`🗑️ Removed label: "${label}"`);
+ } catch (error) {
+ core.warning(`⚠️ Could not remove label "${label}". Error: ${error.message}`);
+ }
+ }
+ }
+
+ const selected = checkedCategories.map(c => c.displayName).join(', ');
+ core.info(`✅ PR categories selected: ${selected}`);
+ core.info(`🏷️ Labels synced: ${labelsToAdd.join(', ')}`);
diff --git a/.github/workflows/pr-cypress-e2e.yml b/.github/workflows/pr-cypress-e2e.yml
new file mode 100644
index 0000000000..abfa9caf51
--- /dev/null
+++ b/.github/workflows/pr-cypress-e2e.yml
@@ -0,0 +1,43 @@
+name: E2E Test
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ push:
+ branches:
+ - master
+
+jobs:
+ e2e:
+ name: Run Cypress E2E Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - name: Run Cypress E2E Tests
+ uses: cypress-io/github-action@v6
+ with:
+ install-command: npm ci
+ start: npm start
+ wait-on: 'http://127.0.0.1:3000'
+ wait-on-timeout: 120
+ browser: chrome
+ config: video=false
+
+ - name: Upload Cypress Screenshots on Failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: cypress-screenshots
+ path: cypress/screenshots
+ if-no-files-found: ignore
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d3007942d8..f0cd008491 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,17 +1,16 @@
## Contributing
We welcome contributions of all kinds — whether it’s code,
-documentation, music, lesson plans, artwork, or ideas. Music Blocks
+documentation, music, lesson plans, artwork, or ideas. Music Blocks
is a community-driven project, and every meaningful contribution helps
improve the platform for learners and educators around the world.
If you’re new to the project, start by setting up the local
-development environment using the guide linked above, then explore
+development environment using the guide linked below, then explore
open issues or discussions to find a place to contribute.
- [How to set up a local server](README.md#how-to-set-up-a-local-server)
-
### Special Notes
Music Blocks is being built from the ground-up, to address several
@@ -50,9 +49,12 @@ following resources:
- [JavaScript tutorial - w3schools.com](https://www.w3schools.com/js/default.asp)
- [JavaScript reference - MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
-Programmers, please follow these general [guidelines for
+For code contributions, please follow these general [guidelines for
contributions](https://github.com/sugarlabs/sugar-docs/blob/master/src/contributing.md).
+### AI guidelines
+
+Follow [AI guidelines for Sugar Labs](https://github.com/sugarlabs/sugar-docs/blob/master/src/contributing.md#ai-guidelines-for-sugar-labs)
### Before You Push
@@ -64,8 +66,20 @@ npx prettier --check . # Formatting
npm test # Jest
```
+NOTE: Only run ```prettier``` on the files you have modified.
+
If formatting fails, run `npx prettier --write .` to fix it.
+### After your PR is merged
+
+Please note that production deployments of Music Blocks are **manual**.
+
+This means that even after your pull request is merged, your changes may not immediately appear. Your update will become visible after the next official release is deployed.
+
+If your changes are not visible right away, it does **not** indicate a problem with your PR or implementation.
+
+This note is included to prevent contributors from spending time debugging caching or deployment issues unnecessarily.
+
### License Header
Music Blocks is licensed under the [AGPL](https://www.gnu.org/licenses/agpl-3.0.en.html).
@@ -129,55 +143,55 @@ Feel free. But, please don't spam :p.
### Keep in Mind
1. Your contributions need not necessarily have to address any
-discovered issue. If you encounter any, feel free to add a fix through
-a PR, or create a new issue ticket.
+ discovered issue. If you encounter any, feel free to add a fix through
+ a PR, or create a new issue ticket.
2. Use [labels](https://github.com/sugarlabs/musicblocks/labels) on
-your issues and PRs.
+ your issues and PRs.
3. Please do not spam with many PRs consisting of little changes.
4. If you are addressing a bulk change, divide your commits across
-multiple PRs, and send them one at a time. The fewer the number of
-files addressed per PR, the better.
+ multiple PRs, and send them one at a time. The fewer the number of
+ files addressed per PR, the better.
5. Communicate effectively. Go straight to the point. You don't need
-to address anyone using '_sir_'. Don't write unnecessary comments;
-don't be over-apologetic. There is no superiority hierarchy. Every
-single contribution is welcome, as long as it doesn't spam or distract
-the flow.
+ to address anyone using '_sir_'. Don't write unnecessary comments;
+ don't be over-apologetic. There is no superiority hierarchy. Every
+ single contribution is welcome, as long as it doesn't spam or distract
+ the flow.
6. Write useful, brief commit messages. Add commit descriptions if
-necessary. PR name should speak about what it is addressing and not
-the issue. In case a PR fixes an issue, use `fixes #ticketno` or
-`closes #ticketno` in the PR's comment. Briefly explain what your PR
-is doing.
+ necessary. PR name should speak about what it is addressing and not
+ the issue. In case a PR fixes an issue, use `fixes #ticketno` or
+ `closes #ticketno` in the PR's comment. Briefly explain what your PR
+ is doing.
7. Always test your changes extensively before creating a PR. There's
-no sense in merging broken code. If a PR is a _work in progress
-(WIP)_, convert it to draft. It'll let the maintainers know it isn't
-ready for merging.
+ no sense in merging broken code. If a PR is a _work in progress
+ (WIP)_, convert it to draft. It'll let the maintainers know it isn't
+ ready for merging.
8. Read and revise the concepts about programming constructs you're
-dealing with. You must be clear about the behavior of the language or
-compiler/transpiler. See [JavaScript
-docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript).
+ dealing with. You must be clear about the behavior of the language or
+ compiler/transpiler. See [JavaScript
+ docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript).
9. If you have a question, do a _web search_ first. If you don't find
-any satisfactory answer, then ask it in a comment. If it is a general
-question about Music Blocks, please use the new
-[discussions](https://github.com/sugarlabs/musicblocks/discussions)
-tab on top the the repository, or the _Sugar-dev Devel
-<[sugar-devel@lists.sugarlabs.org](mailto:sugar-devel@lists.sugarlabs.org)>_
-mailing list. Don't ask silly questions (unless you don't know it is
-silly ;p) before searching it on the web.
+ any satisfactory answer, then ask it in a comment. If it is a general
+ question about Music Blocks, please use the new
+ [discussions](https://github.com/sugarlabs/musicblocks/discussions)
+ tab on top the the repository, or the _Sugar-dev Devel
+ <[sugar-devel@lists.sugarlabs.org](mailto:sugar-devel@lists.sugarlabs.org)>_
+ mailing list. Don't ask silly questions (unless you don't know it is
+ silly ;p) before searching it on the web.
10. Work on things that matter. Follow three milestones: `Port Ready`,
-`Migration`, and `Future`. Those tagged `Port Ready` are
-priority. Those tagged with `Migration` will be taken care of during
-or after the foundation rebuild. Feel free to participate in the
-conversation, adding valuable comments. Those tagged with `Future`
-need not be addressed presently.
+ `Migration`, and `Future`. Those tagged `Port Ready` are
+ priority. Those tagged with `Migration` will be taken care of during
+ or after the foundation rebuild. Feel free to participate in the
+ conversation, adding valuable comments. Those tagged with `Future`
+ need not be addressed presently.
_Please note there is no need to ask permission to work on an
issue. You should check for pull requests linked to an issue you are
@@ -185,4 +199,4 @@ addressing; if there are none, then assume nobody has done
anything. Begin to fix the problem, test, make your commits, push your
commits, then make a pull request. Mention an issue number in the pull
request, but not the commit message. These practices allow the
-competition of ideas (Sugar Labs is a meritocracy)._
\ No newline at end of file
+competition of ideas (Sugar Labs is a meritocracy)._
diff --git a/Docs/PRODUCTION_BUILD_STRATEGY.md b/Docs/PRODUCTION_BUILD_STRATEGY.md
new file mode 100644
index 0000000000..24e4ffc5cc
--- /dev/null
+++ b/Docs/PRODUCTION_BUILD_STRATEGY.md
@@ -0,0 +1,76 @@
+# Production Build Optimization Strategy: Architecture & Performance
+
+## Why
+Music Blocks currently serves mostly unbundled, raw JavaScript and asset files.
+While this works in development, it introduces limitations for:
+- Production performance (high HTTP request count)
+- Predictable offline caching
+- Service worker reliability
+- Long-term maintainability alongside AMD-based loading
+
+This document explores a **lightweight investigation** of production build optimizations without introducing a full migration or breaking existing architecture.
+
+## Current Behavior
+- JavaScript and assets are largely served as individual files.
+- No formal production bundling or minification strategy exists.
+- Service worker caching must account for many independent assets (hundreds of individual JS files).
+- RequireJS / AMD loading is the primary module system, which constrains conventional bundling approaches that expect ES modules or CommonJS.
+
+## Analysis: Current State & Offline Impact
+
+### Baseline Metrics (as of Feb 2026)
+To provide a comparison reference for future optimizations:
+- **Total JavaScript Request Count:** ~248 files (209 Application, 39 Libraries).
+- **Total JavaScript Size (Uncompressed):** ~12.94 MB.
+- **Application Logic (`js/`):** 209 files, 6.42 MB.
+- **Libraries (`lib/`):** 39 files, 6.52 MB.
+- **Loading Model:** Sequential AMD loading, resulting in a deep waterfall of requests.
+
+### Service Worker & Offline Caching
+The current `sw.js` implementation follows a **stale-while-revalidate** pattern with significant constraints for offline reliability:
+1. **Limited Pre-caching:** Only `index.html` is explicitly pre-cached. All other assets are cached dynamically upon first request.
+2. **Fragmentation:** Caching 200+ individual JS files increases the risk of partial cache states (where some files are cached and others are not), leading to runtime errors in offline mode.
+3. **Implicit Dependencies:** If the service worker fails to intercept a single AMD dependency (e.g., due to a network blip), the entire module fails to load.
+4. **Cache Invalidation:** Without content hashing, ensuring users receive the latest version of a file depends on the browser's fetch behavior and the SW's revalidation logic, which can be inconsistent.
+
+### Proposed Strategy (Groundwork)
+This strategy focuses on low-risk, future-oriented thinking:
+
+### 1. Selective Minification
+Before full bundling, we can investigate minifying individual files during a "build" step.
+- Reduces payload size without changing the loading model.
+- Keep source maps for easier production debugging.
+
+### 2. Asset Grouping (Low-Risk Experiment)
+Instead of bundling everything into one file (which breaks RequireJS's dynamic loading), we can group "core" modules that are always loaded together.
+- Example: `js/utils/utils.js`, `js/turtles.js`, and basic library wrappers.
+
+### 3. Hashing/Versioning
+Introduce a simple hashing mechanism for production assets to ensure service workers can reliably identify when an asset has changed.
+
+### Running the Asset Analysis Script
+
+From the repository root:
+
+```bash
+node scripts/analyze-production-assets.js
+```
+
+This script recursively scans the `js/` and `lib/` directories to report:
+- Total JavaScript file count
+- Aggregate size
+- Estimated minification savings
+
+No build step or configuration is required.
+
+## Scope & Constraints
+- **Documentation first:** This document serves as the primary outcome of this phase.
+- **No replacement of RequireJS / AMD:** The current module system is deeply integrated and stable.
+- **No build system overhaul:** Use existing Node.js scripts or lightweight tools if any implementation is attempted.
+- **No runtime behavior changes:** The priority is stability.
+
+## Next Steps / Roadmap
+- [x] Audit total request count and asset sizes.
+- [ ] Implement a lightweight minification pass for `js/` files in the `dist/` task.
+- [ ] Research RequireJS `r.js` optimizer or modern alternatives (like Vite or esbuild) that can target AMD.
+- [ ] Create a manifest for the Service Worker to enable reliable pre-caching of core assets.
diff --git a/css/activities.css b/css/activities.css
index 25622621b7..e8f2aebee7 100644
--- a/css/activities.css
+++ b/css/activities.css
@@ -14,10 +14,15 @@
@import url("play-only-mode.css");
-*:focus {
+*:focus:not(:focus-visible) {
outline: none;
}
+*:focus-visible {
+ outline: 2px solid #0066FF !important;
+ outline-offset: 2px;
+}
+
body:not(.dark) #helpfulSearch,
body:not(.dark) .ui-autocomplete {
background-color: #fff !important;
@@ -509,6 +514,8 @@ div.back:active {
position: absolute;
top: 0;
left: 0;
+
+ height: calc(var(--vh, 1vh) * 100);
}
.canvasHolder.hide {
@@ -623,7 +630,7 @@ nav ul li {
#planet-iframe {
width: 100vw;
- height: 100vh;
+ height: calc(var(--vh, 1vh) * 100);
background-color: #fff;
position: absolute;
top: 0;
@@ -633,6 +640,8 @@ nav ul li {
iframe,
canvas {
overflow: clip !important;
+ width: 100%;
+ height: 100%;
}
.projectname {
@@ -1199,7 +1208,7 @@ table {
left: 0 !important;
border: 0 !important;
overflow-x: hidden;
- overflow-y: auto;
+ overflow: visible;
width: calc(100% - 130px) !important;
max-width: 350px;
padding: 1rem 1rem 0 1rem;
@@ -1209,6 +1218,10 @@ table {
align-items: center;
}
+#helpScrollWrapper {
+ overflow-y: auto;
+}
+
#helpBodyDiv .heading,
#helpBodyDiv .description,
#helpBodyDiv .message {
diff --git a/css/darkmode.css b/css/darkmode.css
index 568284df28..201c080807 100644
--- a/css/darkmode.css
+++ b/css/darkmode.css
@@ -61,4 +61,20 @@ body.dark {
background-color: rgba(255, 255, 255, 0.855);
border-radius: 3px;
padding: 2px;
+}
+
+/* Window table colors for dark mode */
+.dark .windowFrame td {
+ background-color: #022363 !important;
+ color: #eeeeee !important;
+}
+
+.dark .windowFrame td.headcol {
+ background-color: #363636 !important;
+ color: #ffffff !important;
+}
+
+/* Ensure bold text in cells is also light colored */
+.dark .windowFrame td b {
+ color: #ffffff !important;
}
\ No newline at end of file
diff --git a/css/style.css b/css/style.css
index 9e17cb9cc1..218fc1e31c 100644
--- a/css/style.css
+++ b/css/style.css
@@ -114,4 +114,3 @@ input[type="range"]:focus::-ms-fill-upper {
.lego-size-1 { width: 20px; height: 10px; }
.lego-size-2 { width: 40px; height: 10px; }
/* ... more sizes ... */
-
diff --git a/cypress/e2e/main.cy.js b/cypress/e2e/main.cy.js
index 7f95577f49..7ed1948d28 100644
--- a/cypress/e2e/main.cy.js
+++ b/cypress/e2e/main.cy.js
@@ -14,7 +14,9 @@ describe("MusicBlocks Application", () => {
describe("Loading and Initial Render", () => {
it("should display the loading animation and then the main content", () => {
cy.get("#loading-image-container").should("be.visible");
- cy.contains("#loadingText", "Loading Complete!", { timeout: 20000 }).should("be.visible");
+ cy.contains("#loadingText", "Loading Complete!", { timeout: 20000 }).should(
+ "be.visible"
+ );
cy.wait(10000);
cy.get("#canvas", { timeout: 10000 }).should("be.visible");
});
@@ -27,7 +29,7 @@ describe("MusicBlocks Application", () => {
describe("Audio Controls", () => {
it("should have a functional play button", () => {
cy.get("#play").should("be.visible").click();
- cy.window().then((win) => {
+ cy.window().then(win => {
const audioContext = win.Tone.context;
cy.wrap(audioContext.state).should("eq", "running");
});
@@ -46,12 +48,9 @@ describe("MusicBlocks Application", () => {
});
it("should toggle full-screen mode", () => {
- cy.get("#FullScreen").click();
+ cy.get("#FullScreen").should("be.visible").click();
cy.wait(500);
- cy.document().its("fullscreenElement").should("not.be.null");
- cy.get("#FullScreen").click();
- cy.wait(500);
- cy.document().its("fullscreenElement").should("be.null");
+ cy.get("#FullScreen").should("be.visible").click();
});
it("should toggle the toolbar menu", () => {
@@ -81,9 +80,7 @@ describe("MusicBlocks Application", () => {
});
it('should click the New File button and verify "New Project" appears', () => {
- cy.get("#newFile > .material-icons")
- .should("exist")
- .and("be.visible");
+ cy.get("#newFile > .material-icons").should("exist").and("be.visible");
cy.get("#newFile > .material-icons").click();
cy.wait(500);
cy.contains("New project").should("be.visible");
@@ -99,7 +96,7 @@ describe("MusicBlocks Application", () => {
"#Decrease\\ block\\ size > img",
"#Increase\\ block\\ size > img"
];
-
+
bottomBarElements.forEach(selector => {
cy.get(selector).should("exist").and("be.visible");
});
@@ -111,21 +108,14 @@ describe("MusicBlocks Application", () => {
"tr > :nth-child(2) > img",
"tr > :nth-child(3) > img"
];
-
+
sidebarElements.forEach(selector => {
- cy.get(selector)
- .should("exist")
- .and("be.visible")
- .click();
+ cy.get(selector).should("exist").and("be.visible").click();
});
});
it("should verify that Grid, Clear, and Collapse elements exist and are visible", () => {
- const elements = [
- "#Grid > img",
- "#Clear",
- "#Collapse > img"
- ];
+ const elements = ["#Grid > img", "#Clear", "#Collapse > img"];
elements.forEach(selector => {
cy.get(selector).should("exist").and("be.visible");
});
@@ -149,12 +139,12 @@ describe("MusicBlocks Application", () => {
.and("have.attr", "src")
.and("not.be.empty");
- cy.get("#planet-iframe").then(($iframe) => {
+ cy.get("#planet-iframe").then($iframe => {
const iframeSrc = $iframe.attr("src");
cy.log("Iframe source:", iframeSrc);
});
- cy.window().then((win) => {
+ cy.window().then(win => {
win.document.getElementById("planet-iframe").style.display = "block";
});
});
diff --git a/dist/css/style.css b/dist/css/style.css
index 59957a64fe..a731b00aa7 100644
--- a/dist/css/style.css
+++ b/dist/css/style.css
@@ -104,4 +104,30 @@ input[type="range"]:focus::-ms-fill-upper {
position: absolute;
cursor: pointer;
}
+
+/* Keyboard navigation focus state for toolbar buttons */
+.toolbar-btn-focused {
+ background-color: rgba(0, 0, 0, 0.25) !important;
+ border-radius: 4px;
+ transition: background-color 0.15s ease;
+}
+
+/* Dark mode focus - brighten the background */
+body.dark .toolbar-btn-focused,
+.dark-theme .toolbar-btn-focused {
+ background-color: rgba(255, 255, 255, 0.3) !important;
+}
+
+/* Keyboard navigation focus state for dropdown menu items */
+.dropdown-item-focused {
+ background-color: rgba(0, 150, 136, 0.2) !important;
+}
+
+/* Keyboard navigation focus state for modal dialog buttons */
+.modal-btn-focused {
+ outline: 3px solid #009688 !important;
+ outline-offset: 2px;
+ box-shadow: 0 0 8px rgba(0, 150, 136, 0.5) !important;
+}
+
html, body { overscroll-behavior-x: none; }
\ No newline at end of file
diff --git a/env.js b/env.js
new file mode 100644
index 0000000000..2f9995e1e6
--- /dev/null
+++ b/env.js
@@ -0,0 +1,3 @@
+// File for static Musicblocks hosting e.g. https://sugarlabs.github.io/musicblocks/
+window.MB_ENV = "production";
+window.MB_IS_DEV = false;
diff --git a/examples/test-suite.html b/examples/test-suite.html
index 17785b5625..a5066979f8 100644
--- a/examples/test-suite.html
+++ b/examples/test-suite.html
@@ -1 +1 @@
-
To run this project, open Music Blocks in a web browser and drag and drop this file into the browser window. Alternatively, open the file in Music Blocks using the Load project button.
Project Code
This code stores data about the blocks in a project. Show
To run this project, open Music Blocks in a web browser and drag and drop this file into the browser window. Alternatively, open the file in Music Blocks using the Load project button.
Project Code
This code stores data about the blocks in a project.Show