diff --git a/nose/chris/pub/index.html b/nose/chris/pub/index.html
index 578c6d7..08f70db 100644
--- a/nose/chris/pub/index.html
+++ b/nose/chris/pub/index.html
@@ -13,6 +13,9 @@
@defunkt
This is my website. I am Chris.
+
Other.html
+
corey
+
google
diff --git a/nose/chris/pub/other.html b/nose/chris/pub/other.html
new file mode 100644
index 0000000..231a282
--- /dev/null
+++ b/nose/chris/pub/other.html
@@ -0,0 +1,4 @@
+
+
other
+
other
+
\ No newline at end of file
diff --git a/nose/ping/index.ts b/nose/ping/index.ts
deleted file mode 100644
index 01816b7..0000000
--- a/nose/ping/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export default () =>
- "pong"
\ No newline at end of file
diff --git a/nose/ping/index.tsx b/nose/ping/index.tsx
new file mode 100644
index 0000000..bec88b3
--- /dev/null
+++ b/nose/ping/index.tsx
@@ -0,0 +1,2 @@
+export default () =>
+
pong pong - {Date.now()}
\ No newline at end of file
diff --git a/public/browser-nav.js b/public/browser-nav.js
new file mode 100644
index 0000000..ee9afc4
--- /dev/null
+++ b/public/browser-nav.js
@@ -0,0 +1,96 @@
+// browser-nav.js - Injected into NOSE apps to get the UI browser working.
+(function () {
+ const ALLOWED_DOMAINS = ['localhost', '.local', '.nose.space']
+
+ function isAllowedOrigin(url) {
+ try {
+ const urlObj = new URL(url, window.location.href)
+ return ALLOWED_DOMAINS.some(domain =>
+ urlObj.hostname === domain || urlObj.hostname.endsWith(domain)
+ )
+ } catch {
+ return false
+ }
+ }
+
+ // Intercept navigation attempts
+ function interceptNavigation(e) {
+ const target = e.target.closest('a')
+ if (!target || !target.href) return
+
+ // Allow relative URLs and hash links
+ if (target.getAttribute('href').startsWith('#')) return
+ if (target.getAttribute('href').startsWith('/')) return
+
+ // Check if external
+ if (!isAllowedOrigin(target.href)) {
+ e.preventDefault()
+
+ if (window.parent !== window) {
+ window.parent.postMessage({
+ type: 'NAV_BLOCKED',
+ data: { url: target.href, reason: 'External domain not allowed' }
+ }, '*')
+ } else {
+ alert('Navigation to external sites is not allowed')
+ }
+ }
+ }
+
+ // Listen for clicks on links
+ document.addEventListener('click', interceptNavigation, true)
+
+ // Intercept programmatic navigation
+ const originalPushState = history.pushState
+ const originalReplaceState = history.replaceState
+
+ history.pushState = function (state, title, url) {
+ if (url && !isAllowedOrigin(url)) {
+ if (window.parent !== window) {
+ window.parent.postMessage({
+ type: 'NAV_BLOCKED',
+ data: { url, reason: 'External domain not allowed' }
+ }, '*')
+ }
+ return
+ }
+ return originalPushState.apply(this, arguments)
+ }
+
+ history.replaceState = function (state, title, url) {
+ if (url && !isAllowedOrigin(url)) {
+ if (window.parent !== window) {
+ window.parent.postMessage({
+ type: 'NAV_BLOCKED',
+ data: { url, reason: 'External domain not allowed' }
+ }, '*')
+ }
+ return
+ }
+ return originalReplaceState.apply(this, arguments)
+ }
+
+ // Listen for navigation commands from parent
+ window.addEventListener('message', (event) => {
+ console.log(event)
+ if (event.data.type === 'NAV_COMMAND') {
+ switch (event.data.action) {
+ case 'back': history.back(); break
+ case 'forward': history.forward(); break
+ case 'reload': window.location.reload(); break
+ case 'stop': window.stop(); break
+ }
+ }
+ })
+
+ if (window.parent !== window) {
+ window.parent.postMessage({ type: 'NAV_READY' }, '*')
+
+ window.addEventListener('popstate', () => {
+ window.parent.postMessage({
+ type: 'URL_CHANGED',
+ data: { url: location.href }
+ }, '*')
+ })
+ }
+})()
\ No newline at end of file
diff --git a/src/css/browser.css b/src/css/browser.css
new file mode 100644
index 0000000..353f149
--- /dev/null
+++ b/src/css/browser.css
@@ -0,0 +1,54 @@
+:root {
+ --browser-bar-height: 34px;
+}
+
+iframe.browser {
+ display: block;
+ background-color: white;
+ z-index: 10;
+ border: none;
+ margin-top: var(--browser-bar-height);
+}
+
+[data-mode="tall"] iframe.browser {
+ height: 100%;
+}
+
+iframe:focus {
+ outline: none;
+}
+
+.browser.active {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+#browser-controls {
+ position: absolute;
+ top: 0;
+ z-index: 15;
+ width: 100%;
+ height: var(--browser-bar-height);
+ background-color: var(--gray);
+}
+
+#browser-controls span,
+#browser-controls a {
+ display: inline-block;
+ margin-right: 10px;
+ padding: 5px;
+ background-color: var(--gray);
+ color: var(--c64-light-blue);
+ text-decoration: none;
+}
+
+#forward-button {
+ transform: scaleX(-1);
+}
+
+#close-button {
+ position: absolute;
+ right: -10;
+}
\ No newline at end of file
diff --git a/src/css/main.css b/src/css/main.css
index 3cca9cc..7d5fd89 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -16,6 +16,8 @@
--purple: #7C3AED;
--blue: #1565C0;
--magenta: #ff66cc;
+ --gray: #BEBEBE;
+ --grey: #BEBEBE;
--c64-light-blue: #6C6FF6;
--c64-dark-blue: #40318D;
diff --git a/src/html/layout.tsx b/src/html/layout.tsx
index 5401fbb..b7c5e83 100644
--- a/src/html/layout.tsx
+++ b/src/html/layout.tsx
@@ -11,6 +11,7 @@ export const Layout: FC = async ({ children, title }) => (
+
diff --git a/src/html/terminal.tsx b/src/html/terminal.tsx
index 9908d68..6216519 100644
--- a/src/html/terminal.tsx
+++ b/src/html/terminal.tsx
@@ -5,6 +5,15 @@ export const Terminal: FC = async () => (
+
+
+
+
+
@
+
╳
+
chris.nose-pluto.local
+
+
>