diff --git a/packages/cli/assets/web/dev.css b/packages/cli/assets/web/dev.css
new file mode 100644
index 0000000000..4b9978d6ca
--- /dev/null
+++ b/packages/cli/assets/web/dev.css
@@ -0,0 +1,91 @@
+/* Inter Font */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap') layer;
+
+#dx-toast-template {
+ display: none;
+ visibility: hidden;
+}
+
+.dx-toast {
+ position: absolute;
+ top: 10px;
+ right: 0;
+ padding-right: 10px;
+ user-select: none;
+ /* transition: transform 0.2s ease; */
+ z-index: 2147483647;
+}
+
+.dx-toast .dx-toast-inner {
+ /* transition: right 0.2s ease-out; */
+ position: fixed;
+
+ background-color: #181B20;
+ color: #ffffff;
+ font-family: "Inter", sans-serif;
+
+ display: grid;
+ grid-template-columns: auto auto;
+ max-width: 400px;
+ min-height: 56px;
+ border-radius: 5px;
+}
+
+.dx-toast .dx-toast-inner {
+ cursor: pointer;
+ margin-right: 10px;
+}
+
+.dx-toast .dx-toast-level-bar-container {
+ height: 100%;
+ width: 6px;
+}
+
+.dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
+ width: 100%;
+ height: 100%;
+ border-radius: 5px 0px 0px 5px;
+}
+
+.dx-toast .dx-toast-content {
+ padding: 8px;
+}
+
+.dx-toast .dx-toast-header {
+ display: flex;
+ flex-direction: row;
+ justify-content: start;
+ align-items: end;
+ margin-bottom: 10px;
+}
+
+.dx-toast .dx-toast-header>svg {
+ height: 18px;
+ margin-right: 5px;
+}
+
+.dx-toast .dx-toast-header .dx-toast-header-text {
+ font-size: 14px;
+ font-weight: 700;
+ padding: 0;
+ margin: 0;
+}
+
+.dx-toast .dx-toast-msg {
+ font-size: 11px;
+ font-weight: 400;
+ padding: 0;
+ margin: 0;
+}
+
+.dx-toast-level-bar.info {
+ background-color: #428EFF;
+}
+
+.dx-toast-level-bar.success {
+ background-color: #42FF65;
+}
+
+.dx-toast-level-bar.error {
+ background-color: #FF4242;
+}
\ No newline at end of file
diff --git a/packages/cli/assets/web/dev.index.html b/packages/cli/assets/web/dev.index.html
index 48d3d46145..1bdfe833f9 100644
--- a/packages/cli/assets/web/dev.index.html
+++ b/packages/cli/assets/web/dev.index.html
@@ -5,169 +5,8 @@
-
-
+
+
diff --git a/packages/cli/assets/web/dev.js b/packages/cli/assets/web/dev.js
new file mode 100644
index 0000000000..78d34f6e97
--- /dev/null
+++ b/packages/cli/assets/web/dev.js
@@ -0,0 +1,68 @@
+const STORAGE_KEY = "SCHEDULED-DX-TOAST";
+let currentTimeout = null;
+let currentToastId = 0;
+
+// Show a toast, removing the previous one.
+function showDXToast(headerText, message, progressLevel, durationMs) {
+ const decor = document.getElementById("__dx-toast-decor");
+ const text = document.getElementById("__dx-toast-text");
+ const msg = document.getElementById("__dx-toast-msg");
+ const inner = document.getElementById("__dx-toast-inner");
+ const toast = document.getElementById("__dx-toast");
+
+ if (decor) decor.className = `dx-toast-level-bar ${progressLevel}`;
+ if (text) text.innerText = headerText;
+ if (msg) msg.innerText = message;
+ if (inner) inner.style.right = "0";
+ if (toast) {
+ toast.removeAttribute("aria-hidden");
+ toast.addEventListener("click", closeDXToast);
+ }
+
+ // Wait a bit of time so animation plays correctly.
+ setTimeout(
+ () => {
+ let ourToastId = currentToastId;
+ currentTimeout = setTimeout(() => {
+ if (ourToastId == currentToastId) {
+ closeDXToast();
+ }
+ }, durationMs);
+ },
+ 100
+ );
+
+ currentToastId += 1;
+}
+
+// Schedule a toast to be displayed after reload.
+function scheduleDXToast(headerText, message, level, durationMs) {
+ let data = {
+ headerText,
+ message,
+ level,
+ durationMs,
+ };
+
+ let jsonData = JSON.stringify(data);
+ sessionStorage.setItem(STORAGE_KEY, jsonData);
+}
+
+// Close the current toast.
+function closeDXToast() {
+ document.getElementById("__dx-toast-inner").style.right = "-1000px";
+ document.getElementById("__dx-toast").setAttribute("aria-hidden", "true");
+ clearTimeout(currentTimeout);
+}
+
+// Handle any scheduled toasts after reload.
+let potentialData = sessionStorage.getItem(STORAGE_KEY);
+if (potentialData) {
+ sessionStorage.removeItem(STORAGE_KEY);
+ let data = JSON.parse(potentialData);
+ showDXToast(data.headerText, data.message, data.level, data.durationMs);
+}
+
+window.scheduleDXToast = scheduleDXToast;
+window.showDXToast = showDXToast;
+window.closeDXToast = closeDXToast;
\ No newline at end of file
diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs
index ab629bc414..f87acb65a6 100644
--- a/packages/cli/src/build/request.rs
+++ b/packages/cli/src/build/request.rs
@@ -5198,6 +5198,18 @@ impl BuildRequest {
)?;
}
+ if self.is_dev_build() {
+ std::fs::write(
+ self.root_dir().join("dev.css"),
+ include_str!("../../assets/web/dev.css"),
+ )?;
+
+ std::fs::write(
+ self.root_dir().join("dev.js"),
+ include_str!("../../assets/web/dev.js"),
+ )?;
+ }
+
// Write the index.html file with the pre-configured contents we got from pre-rendering
self.write_index_html(assets)?;
@@ -6217,7 +6229,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
/// Replace any special placeholders in the HTML with resolved values
fn replace_template_placeholders(&self, html: &mut String, wasm_path: &str, js_path: &str) {
let base_path = self.base_path_or_default();
- *html = html.replace("{base_path}", base_path);
+ *html = html.replace("{base_path}", &format!("/{base_path}"));
let app_name = &self.executable_name();