diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
index 7b53f4effe7..e2c0528d312 100644
--- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
+++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
@@ -5349,6 +5349,31 @@ private JavaScriptRuntimeEnvironment createRuntimeEnvironment(final WindowExt wi
);
}
+ /// Remembers the application's light/dark choice for the loading screen of the NEXT launch:
+ /// `light` or `dark`, or null/anything else to follow the browser again. The page reads the
+ /// key in `
` before any Java runs (`JavascriptBundleWriter.splashTheme` in the
+ /// ByteCodeTranslator, which also names the key); a build whose `javascript.splash.appearance`
+ /// is `light` or `dark` ignores it.
+ private static void setSplashAppearance(String value) {
+ // The cast stays outside the try: a CHECKCAST is unchecked on ParparVM, so a handler
+ // around it would never run (scripts/check-cast-semantics.sh).
+ WindowExt win = (WindowExt) Window.current();
+ try {
+ JSOImplementations.JSStorage storage = win.getLocalStorage();
+ if (storage == null) {
+ return;
+ }
+ if ("light".equals(value) || "dark".equals(value)) {
+ storage.setItem("cn1.splashAppearance", value);
+ } else {
+ storage.removeItem("cn1.splashAppearance");
+ }
+ } catch (RuntimeException e) {
+ // Storage disabled (private mode, a sandboxed frame): the splash follows the browser.
+ Log.e(e);
+ }
+ }
+
@JSBody(params={"msg"}, script="window.onbeforeunload=function(){return msg;}")
private native static void setBeforeUnloadMessage(String msg);
@@ -5357,6 +5382,9 @@ private JavaScriptRuntimeEnvironment createRuntimeEnvironment(final WindowExt wi
@Override
public void setPlatformHint(String key, String value) {
+ if ("platformHint.javascript.splashAppearance".equalsIgnoreCase(key)) {
+ setSplashAppearance(value);
+ }
if ("platformHint.javascript.beforeUnloadMessage".equalsIgnoreCase(key)) {
if (value == null) {
removeBeforeUnload();
diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java
index 0147e4d636e..adaeba3d29d 100644
--- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java
+++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java
@@ -66,7 +66,18 @@ public interface JSFontMetrics extends JSObject {
JSObject getBounds();
}
+ /// The page's `window.localStorage`, for the few values the page itself reads before any
+ /// Java runs (the loading screen's remembered appearance).
+ public interface JSStorage extends JSObject {
+ void setItem(String key, String value);
+
+ void removeItem(String key);
+ }
+
public interface WindowExt extends JSObject {
+
+ @JSProperty
+ abstract JSStorage getLocalStorage();
@JSProperty
diff --git a/Ports/JavaScriptPort/src/main/webapp/port.js b/Ports/JavaScriptPort/src/main/webapp/port.js
index 182606143d2..9d15a732616 100644
--- a/Ports/JavaScriptPort/src/main/webapp/port.js
+++ b/Ports/JavaScriptPort/src/main/webapp/port.js
@@ -4983,6 +4983,8 @@ const html5HideSplashWorkerSymbol = "cn1_com_codename1_impl_html5_HTML5Implement
if (typeof document !== "undefined"
&& typeof globalThis !== "undefined" && typeof globalThis.jQuery === "function") {
try {
+ // Same as the host handler: the page drops the splash background once started.
+ document.documentElement.setAttribute("data-cn1-started", "");
globalThis.jQuery("div#cn1-splash").fadeOut(100, function() {
globalThis.jQuery(this).remove();
});
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java
new file mode 100644
index 00000000000..796d3cc2478
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+
+package com.codenameone.developerguide.snippets.generated;
+
+import com.codename1.ui.CN;
+
+class WorkingWithJavascriptSplashAppearanceSnippet {
+ void snippet() {
+ // tag::working-with-javascript-splash-appearance[]
+ CN.setProperty("platformHint.javascript.splashAppearance", "dark"); // or "light"
+ CN.setProperty("platformHint.javascript.splashAppearance", null); // follow the browser again
+ // end::working-with-javascript-splash-appearance[]
+ }
+}
diff --git a/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html b/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html
index 093262deb4f..551391157dd 100644
--- a/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html
+++ b/docs/demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html
@@ -1,14 +1,5 @@
// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline.
-// tag::working-with-javascript-html-001[]
-
-
-
-
-
...Loading...
-
-// end::working-with-javascript-html-001[]
-
// tag::working-with-javascript-html-002[]
// end::working-with-javascript-html-002[]
diff --git a/docs/developer-guide/Working-With-Javascript.asciidoc b/docs/developer-guide/Working-With-Javascript.asciidoc
index a489cc6c3d1..e15f8b75041 100644
--- a/docs/developer-guide/Working-With-Javascript.asciidoc
+++ b/docs/developer-guide/Working-With-Javascript.asciidoc
@@ -262,15 +262,44 @@ This tells Apache to proxy all requests for '/app' to the domain https://www.mye
[id="splash_screen", reftext="Customizing the Splash Screen"]
=== Customizing the splash screen
-Since your application may include many resource files, videos, etc., the build-server will generate a splash screen for your app to display while it's loading. This basically shows a progress indicator with your app's icon.
+While the application downloads and starts, the page shows a loading screen: the app icon, an animated progress bar and a "Loading" label. It stays up until the first form is shown.
-You can customize this splash screen by modifying the HTML source inside the *cn1-splash* `div` tag of your app's index.html file:
+Build hints set its look, so the settings survive every build instead of being edited into each generated bundle:
-[source,html]
+[cols="1,1,3",options="header"]
+|===
+|Build hint |Default |Meaning
+
+|`javascript.splash.appearance`
+|`auto` once any dark color is set, otherwise `light`
+|`auto` follows the browser's light/dark preference. `light` or `dark` always uses that set of colors, for an app with a single appearance. An app that sets none of these hints keeps a light loading screen, so a light app never opens dark.
+
+|`javascript.splash.background`, `javascript.splash.textColor`, `javascript.splash.accentColor`
+|`#ffffff`, `#666666`, `#0072ff`
+|The page background, the label and the progress bar in the light appearance.
+
+|`javascript.splash.darkBackground`, `javascript.splash.darkTextColor`, `javascript.splash.darkAccentColor`
+|`#121212`, `#b3b3b3`, `#4da3ff`
+|The same three in the dark appearance.
+
+|`javascript.splash.logo`
+|The app icon
+|A different image, as a path relative to the bundle's `index.html`, or `none` for no image.
+|===
+
+Colors are hex values (`#rgb`, `#rrggbb`, with optional alpha) or CSS color names. The page applies them before it paints anything, and keeps the background until the first form is shown, so a dark app no longer opens on a white page.
+
+An app that lets the user choose its own appearance can have the next launch's loading screen match that choice, even when it differs from the browser's preference:
+
+[source,java]
----
-include::../demos/javascript/src/main/snippets/developer-guide/working-with-javascript.html[tag=working-with-javascript-html-001,indent=0]
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/WorkingWithJavascriptSplashAppearanceSnippet.java[tag=working-with-javascript-splash-appearance,indent=0]
----
+The choice is kept in the browser's local storage and applies only in `auto` mode.
+
+For anything the hints don't cover, the markup is the `cn1-splash` `div` in the generated `index.html`; edit it there if you host the bundle yourself.
+
[id="debugging", reftext="Debugging in Chrome"]
=== Debugging
diff --git a/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java b/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java
index 746b00ba83f..c84810dc559 100644
--- a/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java
+++ b/maven/build-engine/src/main/java/com/codename1/builders/JavaScriptBuildHints.java
@@ -89,6 +89,19 @@ public final class JavaScriptBuildHints {
"javascript.native.theme", "javascript.desktopTheme"
};
+ /// The `javascript.splash.*` hints that style the loading screen, each passed to the
+ /// translator as `codename1.` (see `JavascriptBundleWriter.SplashSettings`, which
+ /// validates them).
+ static final String[] SPLASH_HINTS = {
+ "javascript.splash.appearance", "javascript.splash.background",
+ "javascript.splash.textColor", "javascript.splash.accentColor",
+ "javascript.splash.darkBackground", "javascript.splash.darkTextColor",
+ "javascript.splash.darkAccentColor", "javascript.splash.logo"
+ };
+
+ /// The 192px icon `JavaScriptPwa` writes into every bundle.
+ static final String SPLASH_DEFAULT_LOGO = "_icons/icon-192.png";
+
private JavaScriptBuildHints() {
}
@@ -126,6 +139,20 @@ static List translatorOptions(BuildRequest request) {
if (darkReader != null) {
out.add("-Dcodename1.javascript.darkreaderLock=" + !"false".equalsIgnoreCase(darkReader.trim()));
}
+ for (String hint : SPLASH_HINTS) {
+ String value = request.getArg(hint, null);
+ if (value != null && value.trim().length() > 0) {
+ out.add("-Dcodename1." + hint + "=" + value.trim());
+ }
+ }
+ // The loading screen shows the application's icon unless the developer named another
+ // logo (or none). JavaScriptPwa writes the icon to this path after the translator runs,
+ // from the same request.getIcon(); without an icon it writes a placeholder square, which
+ // identifies nothing, so then there is no logo.
+ String logo = request.getArg("javascript.splash.logo", null);
+ if ((logo == null || logo.trim().length() == 0) && request.getIcon() != null) {
+ out.add("-Dcodename1.javascript.splash.logo=" + SPLASH_DEFAULT_LOGO);
+ }
return out;
}
diff --git a/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java b/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java
index 9f079da8cb1..b7d25274ba5 100644
--- a/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java
+++ b/maven/build-engine/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java
@@ -107,6 +107,29 @@ void translatorGetsTheTitleAndOnlyTheOptOutsThatWereSet() {
assertTrue(opts.contains("-Dcodename1.javascript.darkreaderLock=false"), opts.toString());
}
+ @Test
+ void splashHintsReachTheTranslatorAndTheIconIsTheDefaultLogo() {
+ BuildRequest r = request("javascript.splash.appearance", " dark ",
+ "javascript.splash.darkBackground", "#1e1e1e");
+ List opts = JavaScriptBuildHints.translatorOptions(r);
+ assertTrue(opts.contains("-Dcodename1.javascript.splash.appearance=dark"), opts.toString());
+ assertTrue(opts.contains("-Dcodename1.javascript.splash.darkBackground=#1e1e1e"), opts.toString());
+ // No icon: the PWA step writes a placeholder square, which is not worth showing.
+ assertFalse(opts.toString().contains("splash.logo"), opts.toString());
+
+ r.setIcon(new byte[] {1});
+ opts = JavaScriptBuildHints.translatorOptions(r);
+ assertTrue(opts.contains("-Dcodename1.javascript.splash.logo=" + JavaScriptBuildHints.SPLASH_DEFAULT_LOGO),
+ opts.toString());
+
+ r = request("javascript.splash.logo", "none");
+ r.setIcon(new byte[] {1});
+ opts = JavaScriptBuildHints.translatorOptions(r);
+ assertTrue(opts.contains("-Dcodename1.javascript.splash.logo=none"), opts.toString());
+ assertFalse(opts.contains("-Dcodename1.javascript.splash.logo=" + JavaScriptBuildHints.SPLASH_DEFAULT_LOGO),
+ opts.toString());
+ }
+
@Test
void noHintsShipsOnlyTheLegacyPair() {
assertEquals(set("iOS7Theme", "android_holo_light"), ship(request()));
diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java
index 1a44124a328..3ff9998de98 100644
--- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java
+++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java
@@ -111,6 +111,79 @@ static void register(List h) {
+ "lock in its default Dynamic mode only; its Filter, Filter+ and Static modes "
+ "ignore it. Set to `false` to omit the tag."));
+ h.add(new Hint("javascript.splash.appearance")
+ .group(HintGroup.JAVASCRIPT)
+ .values("SplashAppearance", "auto", "light", "dark")
+ .platform("javascript")
+ .doc("Which colors the loading screen uses. Defaults to `auto` when any "
+ + "`javascript.splash.dark*` color is set and to `light` otherwise, so an app "
+ + "that never asked for a dark loading screen doesn't get one. `auto` follows the "
+ + "browser's light/dark preference, or the choice the app remembered with "
+ + "`Display.setProperty(\"platformHint.javascript.splashAppearance\", \"dark\")`. "
+ + "`light` and `dark` always use that set, for apps with one appearance."));
+
+ h.add(new Hint("javascript.splash.background")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .def("#ffffff")
+ .platform("javascript")
+ .doc("The loading screen's background in light appearance, as a hex color (`#rgb`, `#rrggbb`, "
+ + "with optional alpha) or a CSS color name. Applied from the first frame the "
+ + "page paints until the first form shows."));
+
+ h.add(new Hint("javascript.splash.textColor")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .def("#666666")
+ .platform("javascript")
+ .doc("The loading screen's text color in light appearance, as a hex color (`#rgb`, `#rrggbb`, "
+ + "with optional alpha) or a CSS color name. Applied from the first frame the "
+ + "page paints until the first form shows."));
+
+ h.add(new Hint("javascript.splash.accentColor")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .def("#0072ff")
+ .platform("javascript")
+ .doc("The loading screen's progress bar color in light appearance, as a hex color (`#rgb`, `#rrggbb`, "
+ + "with optional alpha) or a CSS color name. Applied from the first frame the "
+ + "page paints until the first form shows."));
+
+ h.add(new Hint("javascript.splash.darkBackground")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .def("#121212")
+ .platform("javascript")
+ .doc("The loading screen's background in dark appearance, as a hex color (`#rgb`, `#rrggbb`, "
+ + "with optional alpha) or a CSS color name. Applied from the first frame the "
+ + "page paints until the first form shows."));
+
+ h.add(new Hint("javascript.splash.darkTextColor")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .def("#b3b3b3")
+ .platform("javascript")
+ .doc("The loading screen's text color in dark appearance, as a hex color (`#rgb`, `#rrggbb`, "
+ + "with optional alpha) or a CSS color name. Applied from the first frame the "
+ + "page paints until the first form shows."));
+
+ h.add(new Hint("javascript.splash.darkAccentColor")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .def("#4da3ff")
+ .platform("javascript")
+ .doc("The loading screen's progress bar color in dark appearance, as a hex color (`#rgb`, `#rrggbb`, "
+ + "with optional alpha) or a CSS color name. Applied from the first frame the "
+ + "page paints until the first form shows."));
+
+ h.add(new Hint("javascript.splash.logo")
+ .group(HintGroup.JAVASCRIPT)
+ .type(HintType.STRING)
+ .platform("javascript")
+ .doc("The image the loading screen shows above the progress bar, as a path relative to "
+ + "the bundle's `index.html` (letters, digits, `/`, `.`, `-` and `_`). Defaults to the "
+ + "app icon; `none` shows no image."));
+
h.add(new Hint("javascript.desktopTheme")
.group(HintGroup.JAVASCRIPT)
.type(HintType.STRING)
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java
index e7e196e75a5..66607c21c39 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java
@@ -1693,6 +1693,7 @@ private static void writeIndex(File outputDirectory) throws IOException {
System.getProperty(PAGE_TITLE_PROPERTY),
Boolean.parseBoolean(Util.systemProperty(ALLOW_TRANSLATION_PROPERTY, "false")),
!"false".equalsIgnoreCase(Util.systemProperty(DARKREADER_LOCK_PROPERTY, "true")));
+ index = applySplashSettings(index, SplashSettings.fromSystemProperties());
Util.writeUtf8(new File(outputDirectory, "index.html"), index);
}
@@ -1741,6 +1742,206 @@ static String applyPageSettings(String index, String title, boolean allowTransla
.replace("__APP_TITLE__", escapeHtml(pageTitle));
}
+ /// Prefix of the translator properties the `javascript.splash.*` build hints become.
+ static final String SPLASH_PROPERTY_PREFIX = "codename1.javascript.splash.";
+
+ /// Local storage key an application writes, through the
+ /// `platformHint.javascript.splashAppearance` property, to have the next launch's loading
+ /// screen follow its own light/dark choice instead of the browser's. `HTML5Implementation`
+ /// writes it; the inline script [#splashTheme(SplashSettings)] emits reads it before the
+ /// first paint.
+ static final String SPLASH_APPEARANCE_STORAGE_KEY = "cn1.splashAppearance";
+
+ /// The loading screen's look: the logo and the colours for each appearance. Every value is
+ /// validated here, because each one is written verbatim into the page's CSS or markup.
+ static final class SplashSettings {
+ /// `auto` (follow the browser, or the app's remembered choice), `light` or `dark`.
+ final String appearance;
+ final String background;
+ final String text;
+ final String accent;
+ final String darkBackground;
+ final String darkText;
+ final String darkAccent;
+ /// A URL relative to the bundle root, or null for no logo.
+ final String logo;
+
+ SplashSettings(String appearance, String background, String text, String accent,
+ String darkBackground, String darkText, String darkAccent, String logo) {
+ // With no appearance and no dark colour named, the splash stays light: an existing
+ // light app must not gain a dark loading screen ahead of its white first form just
+ // because the browser prefers dark. Naming any dark colour is what says the app has
+ // a dark appearance, and makes `auto` the default.
+ boolean darkNamed = notBlank(darkBackground) || notBlank(darkText) || notBlank(darkAccent);
+ this.appearance = appearance(appearance, darkNamed ? "auto" : "light");
+ this.background = color("background", background, "#ffffff");
+ this.text = color("textColor", text, "#666666");
+ this.accent = color("accentColor", accent, "#0072ff");
+ this.darkBackground = color("darkBackground", darkBackground, "#121212");
+ this.darkText = color("darkTextColor", darkText, "#b3b3b3");
+ this.darkAccent = color("darkAccentColor", darkAccent, "#4da3ff");
+ this.logo = logo(logo);
+ }
+
+ static SplashSettings defaults() {
+ return new SplashSettings(null, null, null, null, null, null, null, null);
+ }
+
+ static SplashSettings fromSystemProperties() {
+ return new SplashSettings(prop("appearance"), prop("background"), prop("textColor"),
+ prop("accentColor"), prop("darkBackground"), prop("darkTextColor"),
+ prop("darkAccentColor"), prop("logo"));
+ }
+
+ private static String prop(String name) {
+ return System.getProperty(SPLASH_PROPERTY_PREFIX + name);
+ }
+
+ private static boolean notBlank(String v) {
+ return v != null && v.trim().length() > 0;
+ }
+
+ private static String appearance(String value, String def) {
+ if (value == null || value.trim().length() == 0) {
+ return def;
+ }
+ String v = value.trim();
+ if (v.equalsIgnoreCase("auto") || v.equalsIgnoreCase("light") || v.equalsIgnoreCase("dark")) {
+ return asciiLower(v);
+ }
+ throw new IllegalArgumentException("javascript.splash.appearance must be auto, light or dark, not '"
+ + value + "'");
+ }
+
+ /// A hex colour (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`) or a CSS colour keyword. Nothing
+ /// else: `rgb(...)` and friends would need a parser to be sure they cannot close the
+ /// declaration, and the hex forms express every colour they can.
+ private static String color(String hint, String value, String def) {
+ if (value == null || value.trim().length() == 0) {
+ return def;
+ }
+ String v = value.trim();
+ boolean ok;
+ if (v.charAt(0) == '#') {
+ int n = v.length() - 1;
+ ok = n == 3 || n == 4 || n == 6 || n == 8;
+ for (int i = 1; ok && i < v.length(); i++) {
+ char c = v.charAt(i);
+ ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+ }
+ } else {
+ // A name has to be a real one: a misspelt name, or a keyword such as `inherit`,
+ // makes the variable invalid where it is used, and the page falls back to a
+ // transparent background with no error anywhere.
+ ok = CSS_COLOR_NAMES.indexOf("," + asciiLower(v) + ",") >= 0;
+ }
+ if (!ok) {
+ throw new IllegalArgumentException("javascript.splash." + hint
+ + " must be a hex colour such as #1e1e1e or a CSS named colour, not '" + value + "'");
+ }
+ return v;
+ }
+
+ /// The CSS named colours (CSS Color Module Level 4), plus `transparent`.
+ private static final String CSS_COLOR_NAMES = ",transparent,aliceblue,antiquewhite,aqua,"
+ + "aquamarine,azure,beige,bisque,black,blanchedalmond,blue,blueviolet,brown,burlywood,"
+ + "cadetblue,chartreuse,chocolate,coral,cornflowerblue,cornsilk,crimson,cyan,darkblue,"
+ + "darkcyan,darkgoldenrod,darkgray,darkgreen,darkgrey,darkkhaki,darkmagenta,"
+ + "darkolivegreen,darkorange,darkorchid,darkred,darksalmon,darkseagreen,darkslateblue,"
+ + "darkslategray,darkslategrey,darkturquoise,darkviolet,deeppink,deepskyblue,dimgray,"
+ + "dimgrey,dodgerblue,firebrick,floralwhite,forestgreen,fuchsia,gainsboro,ghostwhite,"
+ + "gold,goldenrod,gray,green,greenyellow,grey,honeydew,hotpink,indianred,indigo,ivory,"
+ + "khaki,lavender,lavenderblush,lawngreen,lemonchiffon,lightblue,lightcoral,lightcyan,"
+ + "lightgoldenrodyellow,lightgray,lightgreen,lightgrey,lightpink,lightsalmon,"
+ + "lightseagreen,lightskyblue,lightslategray,lightslategrey,lightsteelblue,lightyellow,"
+ + "lime,limegreen,linen,magenta,maroon,mediumaquamarine,mediumblue,mediumorchid,"
+ + "mediumpurple,mediumseagreen,mediumslateblue,mediumspringgreen,mediumturquoise,"
+ + "mediumvioletred,midnightblue,mintcream,mistyrose,moccasin,navajowhite,navy,oldlace,"
+ + "olive,olivedrab,orange,orangered,orchid,palegoldenrod,palegreen,paleturquoise,"
+ + "palevioletred,papayawhip,peachpuff,peru,pink,plum,powderblue,purple,rebeccapurple,"
+ + "red,rosybrown,royalblue,saddlebrown,salmon,sandybrown,seagreen,seashell,sienna,"
+ + "silver,skyblue,slateblue,slategray,slategrey,snow,springgreen,steelblue,tan,teal,"
+ + "thistle,tomato,turquoise,violet,wheat,white,whitesmoke,yellow,yellowgreen,";
+
+ /// `none` (or nothing) for no logo; otherwise a path inside the bundle. The file is not
+ /// checked: the translator runs before the bundle is complete (the default icon is
+ /// written by the builder afterwards). A missing one costs nothing visible, since the
+ /// `` carries `alt=""` and browsers draw nothing for an empty-alt image that fails. Restricted to
+ /// plain path characters so it can go into a `src` attribute as it is, and never `..`
+ /// or a scheme, so it cannot point the page at another origin.
+ private static String logo(String value) {
+ if (value == null || value.trim().length() == 0 || value.trim().equalsIgnoreCase("none")) {
+ return null;
+ }
+ String v = value.trim();
+ boolean ok = v.charAt(0) != '/' && v.indexOf("..") < 0;
+ for (int i = 0; ok && i < v.length(); i++) {
+ char c = v.charAt(i);
+ ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
+ || c == '/' || c == '.' || c == '-' || c == '_';
+ }
+ if (!ok) {
+ throw new IllegalArgumentException("javascript.splash.logo must be a path inside the web "
+ + "bundle (letters, digits, '/', '.', '-', '_'), not '" + value + "'");
+ }
+ return v;
+ }
+ }
+
+ private static String asciiLower(String s) {
+ StringBuilder out = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ out.append(c >= 'A' && c <= 'Z' ? (char) (c + ('a' - 'A')) : c);
+ }
+ return out.toString();
+ }
+
+ /// Fills the loading screen's placeholders: its colours and its logo.
+ ///
+ /// This adds an inline `");
+ } else {
+ out.append("");
+ }
+ return out.toString();
+ }
+
+ private static String vars(String background, String text, String accent) {
+ return "--cn1-splash-background:" + background + ";--cn1-splash-text:" + text
+ + ";--cn1-splash-accent:" + accent + ";";
+ }
+
private static String escapeHtml(String text) {
StringBuilder out = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js
index 6036e185a70..a018945aea1 100644
--- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js
+++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js
@@ -4118,6 +4118,12 @@
w.parent.postMessage({ type: 'cn1-app-ready' }, '*');
}
} catch (_pmErr) { /* cross-origin parent -- ignore */ }
+ // The page background is the splash colour only while the splash is up; from the
+ // first form on, the page is transparent again as it always was, so an embed and any
+ // area the canvas leaves uncovered show what they showed before (index.html).
+ if (doc.documentElement) {
+ doc.documentElement.setAttribute('data-cn1-started', '');
+ }
var splash = doc.getElementById('cn1-splash');
if (!splash) {
return null;
diff --git a/vm/ByteCodeTranslator/src/javascript/index.html b/vm/ByteCodeTranslator/src/javascript/index.html
index 2a4b0587370..9f275120358 100644
--- a/vm/ByteCodeTranslator/src/javascript/index.html
+++ b/vm/ByteCodeTranslator/src/javascript/index.html
@@ -5,26 +5,61 @@
__APP_TITLE__
+