Browser history

Browser history demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import '@expressive/react';
import { BrowserRouter, Link, Route } from '@expressive/router';
import type { ReactNode } from 'react';

export default () => (
  <div className="container">
    <h1>Browser history</h1>
    <p>
      <code>BrowserRouter</code> binds the router to the address bar and browser
      history. A nested <code>none</code> Route catches misses inside its section;
      the root one catches the rest.
    </p>
    <BrowserRouter>
      <Route as={Frame}>
        <Route as={Home} />
        <Route to="projects" as={Projects}>
          <Route as={ProjectIndex} />
          <Route to=":id" as={Project} />
          <Route none as={ProjectNotFound} />
        </Route>
        <Route none as={NotFound} />
      </Route>
    </BrowserRouter>
    <small>
      Browser Back and Forward walk these pages - the URL is the source of
      truth. The projects layout persists across every page
      below <code>/projects</code>, its own not-found page included.
    </small>
  </div>
);

const Frame = (props: { children?: ReactNode }) => {
  const { url } = BrowserRouter.get();

  return (
    <section className="browser">
      <code className="address">{url}</code>
      <nav className="nav">
        <Link to="/">Home</Link>
        <Link to="/projects">Projects</Link>
        <Link to="/projects/ada">Ada</Link>
        <Link to="/projects/ada/files">Section miss</Link>
        <Link to="/elsewhere">App miss</Link>
      </nav>
      <div className="view">{props.children}</div>
    </section>
  );
};

const Home = () => <p>Choose a destination.</p>;

const Projects = (props: { children?: ReactNode }) => (
  <section className="project">
    <b>Projects</b>
    {props.children}
  </section>
);

const ProjectIndex = () => <p>Select a project.</p>;

const Project = () => {
  const { match } = Route.get(true);

  return <p>Project: {match!.id}</p>;
};

const ProjectNotFound = () => <p>No project page matches this URL.</p>;

const NotFound = () => <p>No application page matches this URL.</p>;

App.css

.browser {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s3);
  width: 100%;
}

.address {
  width: 100%;
  padding: var(--s2) var(--s3);
  color: var(--accent);
  background: var(--surface-2);
  border-radius: var(--r-sm);
}

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
}

.nav a {
  color: var(--accent);
  text-decoration: none;
}

.nav a:hover {
  text-decoration: underline;
}

.view {
  width: 100%;
  min-height: 6rem;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
}

.view p {
  margin: var(--s2) 0 0;
}

.project {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}