Adds routing

Squashed commit of the following:

commit d26bffc33a
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Tue Jun 9 10:35:14 2026 +0200

     ups versions

commit 8bb12aaac1
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Fri May 22 15:56:44 2026 +0200

     📺 finlizes routing and improves bs

commit c3782509e8
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Wed May 20 16:10:51 2026 +0200

     first steps (footer)

commit 7c925d1fdb
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Wed May 20 15:24:20 2026 +0200

     📺 tsc-alias

commit e4e41197b9
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Wed May 20 14:40:47 2026 +0200

    🐛 fixes routing

commit 7d6240e28a
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Wed May 20 14:40:00 2026 +0200

    🐛 📺 fixes dev server

commit 139b1590ce
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Wed Apr 29 13:40:40 2026 +0200

     Uses original router

    - it has benefits to use hash properly

commit 3cc11b68de
Author: Jan Andrle <andrle.jan@centrum.cz>
Date:   Tue Apr 28 17:16:00 2026 +0200

    🎉
This commit is contained in:
2026-06-09 10:39:23 +02:00
parent 0191347312
commit f440e53a17
36 changed files with 752 additions and 372 deletions
+9
View File
@@ -0,0 +1,9 @@
import { css } from "lit";
export const styles = css`
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
`;
+14
View File
@@ -0,0 +1,14 @@
import { LitElement, html } from "lit";
import { property, customElement } from "lit/decorators.js";
import { styles } from "./index.css.js";
@customElement("app-episode")
export class AppEpisode extends LitElement {
static override styles = styles;
@property({ type: String }) override id = "";
override render() {
return html`
Episode ${this.id}
`;
}
}
+16
View File
@@ -0,0 +1,16 @@
import { route } from "@/core/route.js";
import { html } from "lit";
export const path = "/episodes/" as const;
export const routes = route(
{
path: "/episodes/:id/",
async enter() {
await import("./index.js");
return true;
},
render({ id }){
return html`<app-episode id=${id}></app-episode>`;
},
},
);
+9
View File
@@ -0,0 +1,9 @@
import { css } from "lit";
export const styles = css`
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
`;
+14
View File
@@ -0,0 +1,14 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { styles } from "./index.css.js";
@customElement("app-episodes")
export class AppEpisodes extends LitElement {
static override styles = styles;
override render() {
return html`
<div class="logo"></div>
<a href="/episodes/1">Episode 1</a>
`;
}
}
+18
View File
@@ -0,0 +1,18 @@
import { route } from "@/core/route.js";
import { routes as routeEpisode } from "./:id/routes.js";
import { html } from "lit";
export const path = "/episodes/" as const;
export const routes = route(
{
path,
async enter() {
await import("./index.js");
return true;
},
render(){
return html`<app-episodes></app-episodes>`;
},
},
routeEpisode,
);
+9
View File
@@ -0,0 +1,9 @@
import { css } from "lit";
export const styles = css`
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
`;
+22
View File
@@ -0,0 +1,22 @@
import { html } from "lit";
import { fixture, expect } from "@open-wc/testing";
import type { AppHome } from "./index.js";
import "./index.js";
describe("AppHome", () => {
let element: AppHome;
beforeEach(async () => {
element = await fixture(html`<app-home></app-home>`);
});
it("renders a h1", () => {
const h1 = element.shadowRoot!.querySelector('h1')!;
expect(h1).to.exist;
expect(h1.textContent).to.equal('My app');
});
it("passes the a11y audit", async () => {
await expect(element).shadowDom.to.be.accessible();
});
});
+15
View File
@@ -0,0 +1,15 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { styles } from "./index.css.js";
@customElement("app-home")
export class AppHome extends LitElement {
static override styles = styles;
override render() {
return html`
<h1>My app</h1>
<p>Hello world</p>
<a href="/episodes">Episode</a>
`;
}
}
+16
View File
@@ -0,0 +1,16 @@
import { route } from "@/core/route.js";
import { html } from "lit";
export const path = "/" as const;
export const routes = route(
{
path,
async enter() {
await import("./index.js");
return true;
},
render(){
return html`<app-home></app-home>`;
},
},
);
-33
View File
@@ -1,33 +0,0 @@
import { css } from "lit";
export const styles = css`
:host {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
font-size: calc(10px + 2vmin);
color: #1a2b42;
max-width: 960px;
margin: 0 auto;
text-align: center;
background-color: var(--app-cfpodcasts-background-color);
}
main {
flex-grow: 1;
}
.logo {
margin-top: 36px;
}
.app-footer {
font-size: calc(12px + 0.5vmin);
align-items: center;
}
.app-footer a {
margin-left: 5px;
}
`;
-39
View File
@@ -1,39 +0,0 @@
import { LitElement, html } from "lit";
import { property, customElement } from "lit/decorators.js";
import { styles } from "./app-index.css.js";
const logo = import.meta.resolve("../../assets/logo.svg");
@customElement("app-cfpodcasts")
export class AppCfpodcasts extends LitElement {
@property({ type: String }) header = "My app";
static styles = styles;
render() {
return html`
<main>
<div class="logo"><img alt="open-wc logo" src=${logo} /></div>
<h1>${this.header}</h1>
<p>Edit <code>src/AppCfpodcasts.ts</code> and save to reload.</p>
<a
class="app-link"
href="https://open-wc.org/guides/developing-components/code-examples"
target="_blank"
rel="noopener noreferrer"
>
Code examples
</a>
</main>
<p class="app-footer">
🚽 Made with love by
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/open-wc"
>open-wc</a
>.
</p>
`;
}
}
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="393.84613"
width="393.84613"
viewBox="0 0 5.0480766 5.0480766"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<rect
fill="#f88a14"
rx="0.53846151"
ry="0.53846151"
height="5.0480766"
width="5.0480766"
id="rect1"
x="0"
y="0"
style="stroke-width:0.769231" />
<path
d="m 1.3450904,0.64548657 c 2.9002,0 2.9002,2.91010003 2.9002,2.91010003"
fill="none"
stroke="#ffffff"
stroke-linecap="round"
stroke-width="0.78125"
id="path1" />
<path
d="m 1.3377904,1.9915866 c 1.5705,-0.00908 1.5705,1.5639 1.5705,1.5639"
fill="none"
stroke="#ffffff"
stroke-linecap="round"
stroke-width="0.78125"
id="path2" />
<path
d="m 2.0192904,3.5227866 c 0,0.23366 -0.10712,0.47418 -0.24663,0.6537 -0.1814,0.2333 -0.5705,0.5618 -0.6913,0.5653 0.0402,-0.0662 0.263,-0.5654 0.2563,-0.5654 -0.36423004,0 -0.65950004,-0.29265 -0.65950004,-0.65365 0,-0.361 0.29527,-0.65365 0.65950004,-0.65365 0.36423,0 0.68159,0.29265 0.68159,0.65365 z"
fill="#ffffff"
id="path3"
style="fill:#c83737" />
<path
style="fill:#ffffff;stroke:#666666;stroke-width:0"
d="m 1.0262395,3.0457825 v 0.944372 L 1.8837176,3.5592476 Z"
id="path4" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+24
View File
@@ -0,0 +1,24 @@
import { LitElement, css, html } from "lit";
import { customElement } from "lit/decorators.js";
const styles = css`
:host {
position: absolute;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
padding: 0;
border: 0;
height: 1px;
width: 1px;
overflow: hidden;
}
`;
@customElement("c-sronly")
export class CSronly extends LitElement {
static override styles = styles;
override render() {
return html`
<slot></slot>
`;
}
}
+15
View File
@@ -0,0 +1,15 @@
import type { PathRouteConfig } from "@lit-labs/router";
export function route(...routes: (PathRouteConfig|PathRouteConfig[])[]): PathRouteConfig[] {
return routes.flatMap(function process(route){
if (Array.isArray(route)) // already processed
return route;
const { path, ...rest }= route;
if (!path.endsWith("/"))
throw new Error("path must end with „/”!");
return [
{ path, ...rest },
{ path: path.slice(0, -1), ...rest },
];
});
}
+37
View File
@@ -0,0 +1,37 @@
import { css } from "lit";
export const styles = css`
:host {
height: 100vh;
height: 100dvh;
display: grid;
grid-template-areas:
"main"
"footer";
grid-template-rows: auto 4ch;
overflow: hidden;
}
main {
grid-area: main;
overflow: scroll;
}
nav {
grid-area: footer;
display: flex;
flex-flow: row nowrap;
justify-content: space-evenly;
align-items: center;
padding: .5ch;
a {
display: block;
height: 100%;
aspect-ratio: 1;
}
img {
height: 100%;
}
}
`;
+27
View File
@@ -0,0 +1,27 @@
import { html } from "lit";
import { fixture, expect } from "@open-wc/testing";
import type { AppCfpodcasts } from "./index.js";
import "./index.js";
describe("App", () => {
let element: AppCfpodcasts;
let nav: HTMLElement;
beforeEach(async () => {
element = await fixture(html`<app-cfpodcasts></app-cfpodcasts>`);
nav = element.shadowRoot!.querySelector("nav")!;
});
it("renders <main>", () => {
const content = element.shadowRoot!.querySelector('main')!;
expect(content).to.exist;
});
it("has all pages", async () => {
const links = nav.querySelectorAll("a");
await expect(links.length).to.equal(2);
});
it("passes the a11y audit", async () => {
await expect(nav).shadowDom.to.be.accessible();
});
});
+39
View File
@@ -0,0 +1,39 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { styles } from "./index.css.js";
import { Router } from "@lit-labs/router";
import * as routeHome from "./app-home/routes.js";
import * as routeEpisodes from "./app-episodes/routes.js";
import "./components/c-sronly.js";
const logo = new URL("./assets/logo.svg", import.meta.url);
@customElement("app-cfpodcasts")
export class AppCfpodcasts extends LitElement {
static override styles = styles;
private _routes = new Router(this, [
...routeHome.routes,
...routeEpisodes.routes,
]);
override render() {
console.log(this._routes); // TODO
return html`
<main>${this._routes.outlet()}</main>
<nav>
<a
href="${this._routes.link("/")}"
title="Home"
>
<img alt="" src=${logo} />
<c-sronly>Home</c-sronly>
</a>
<a
href="${this._routes.link(routeEpisodes.path)}"
>
Episodes
</a>
</nav>
`;
}
}