# 08 | Teloce HTML shell and scoped CSS

## Goal

Serve a browser shell and placeholder pages from the existing backend.

## What you are building

app.use_teloce registers the compiler, runtime and browser routes. request.compile returns the compiled app.html shell for known page URLs. Each .html file has a template, script lang=ts and style scoped. The shell provides data-teloce-router-view. Module ui_routes maps compiled page names to browser paths. The first account/project pages are labelled placeholders; we replace them in the next two chapters. Shared types.ts describes JSON contracts and api.ts includes cookies, CSRF and error handling. Scoped CSS belongs to each component, so there is no separate app.css file.

## Build this chapter

Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.

## Step 1: CREATE - make parent folders, then create this file - ui/types.ts

Match the Python response fields exactly. These types document the browser contract; runtime validation stays on the server.

Create the parent directories, then save this complete file.

```typescript
export interface User {
  id: number;
  name: string;
  email: string;
}
export interface Project {
  id: number;
  owner_id: number;
  name: string;
  description: string;
}
export type TaskStatus = "todo" | "doing" | "done";
export interface Task {
  id: number;
  project_id: number;
  title: string;
  status: TaskStatus;
  due_date: string | null;
}
export interface Session {
  user: User | null;
  csrf: string;
}
export interface Article {
  id: string;
  title: string;
  slug: string;
  summary: string;
  body?: string;
}
```

**Say:** "Match the Python response fields exactly. These types document the browser contract; runtime validation stays on the server."

**Type:** Follow the action above in `ui/types.ts`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 2: CREATE - make parent folders, then create this file - ui/api.ts

Centralize fetch, cookies, CSRF rotation and error messages here so pages share one contract.

Create the parent directories, then save this complete file.

```typescript
import type { Session } from "./types";

let session: Session | null = null;
export async function loadSession(): Promise<Session> {
  const response = await fetch("/api/auth/session", { credentials: "same-origin" });
  const payload = await response.json();
  if (!response.ok)
    throw new Error(payload.error?.message || "Could not load your session.");
  session = payload.data;
  return session!;
}

export async function api<T>(path: string, method = "GET", body?: unknown): Promise<T> {
  if (!session) await loadSession();
  const response = await fetch(path, {
    method,
    credentials: "same-origin",
    headers: { "Content-Type": "application/json", "X-CSRF-Token": session!.csrf },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const payload = await response.json();
  if (!response.ok) {
    if (response.status === 401 || response.status === 403) session = null;
    throw new Error(payload.error?.message || "The request failed. Please try again.");
  }
  if (path.startsWith("/api/auth/")) session = payload.data;
  return payload.data as T;
}
```

**Say:** "Centralize fetch, cookies, CSRF rotation and error messages here so pages share one contract."

**Type:** Follow the action above in `ui/api.ts`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 3: EDIT - replace the entire file - ui/app.html

This is the SPA shell. Its internal anchors and router view cooperate with each module's page mappings.

Select all existing contents of this file and replace them with the complete code below.

```html
<template>
  <div class="app-shell">
    <header>
      <a href="/" data-teloce-link class="brand">Project Manager</a>
      <nav aria-label="Main navigation">
        <a href="/projects" data-teloce-link>Projects</a>
        <a href="/login" data-teloce-link>Account</a>
      </nav>
    </header>
    <main id="router-view" data-teloce-router-view></main>
    <footer>Built with Flaxon and Teloce. <a href="/admin/">Staff Admin</a></footer>
  </div>
</template>
<script lang="ts">
export default {};
</script>
<style scoped>
/* Only the document reset is global: body is outside this component. */
:global(body) {
  margin: 0;
}
.app-shell {
  min-height: 100vh;
  font-family: system-ui, sans-serif;
  color: #17243a;
  background: #f3f6fc;
}
header {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: center;
  padding: 1.2rem max(1rem, calc((100vw - 1080px) / 2));
  background: #17243a;
  color: white;
}
header a {
  color: white;
  text-decoration: none;
}
.brand {
  font-weight: 800;
  font-size: 1.3rem;
}
nav {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}
main {
  max-width: 1080px;
  margin: auto;
  min-height: 75vh;
  padding: 2rem 1rem;
}
footer {
  padding: 1.5rem;
  text-align: center;
  color: #526079;
}
footer a {
  color: #1d4ed8;
}
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
@media (max-width: 520px) {
  header {
    flex-direction: column;
    align-items: flex-start;
  }
  main {
    padding: 1rem;
  }
}
</style>
```

**Say:** "This is the SPA shell. Its internal anchors and router view cooperate with each module's page mappings."

**Type:** Follow the action above in `ui/app.html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 4: CREATE - make parent folders, then create this file - ui/pages/Home.html

This file owns the feature named by its module or component. Read the route/form flow before continuing.

Create the parent directories, then save this complete file.

```html
<template>
  <section class="hero">
    <p class="eyebrow">A practical Python full-stack course</p>
    <h1>Make room for your next idea.</h1>
    <p>Create projects, break them into tasks, and watch your progress.</p>
    <a class="button" href="/projects" data-teloce-link>Open projects</a>
    <a href="/login" data-teloce-link>Create an account</a>
  </section>
</template>
<script lang="ts">
export default {};
</script>
<style scoped>
* {
  box-sizing: border-box;
}
a {
  color: #1d4ed8;
}
h1 {
  font-size: clamp(1.8rem, 5vw, 3rem);
  line-height: 1.15;
}
h2 {
  font-size: 1.2rem;
}
p {
  line-height: 1.6;
}
[role="alert"] {
  color: #a21c2b;
  padding: 0.8rem;
  border-left: 4px solid;
}
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
.hero {
  max-width: 700px;
  padding: 3rem 0;
}
.eyebrow {
  text-transform: uppercase;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
}
.button {
  display: inline-block;
  border-radius: 7px;
  min-height: 44px;
  background: #2563eb;
  color: white;
  padding: 0.7rem 1rem;
  text-decoration: none;
  margin-right: 0.5rem;
}
</style>
```

**Say:** "This file owns the feature named by its module or component. Read the route/form flow before continuing."

**Type:** Follow the action above in `ui/pages/Home.html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 5: CREATE - make parent folders, then create this file - modules/auth/ui/pages/Login.html

Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript.

Create the parent directories, then save this complete file.

```html
<template><section><h1>Account</h1><p>This screen is built in the next UI lesson.</p></section></template>
<script lang="ts">export default {};</script>
<style scoped>
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
```

**Say:** "Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript."

**Type:** Follow the action above in `modules/auth/ui/pages/Login.html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 6: CREATE - make parent folders, then create this file - modules/projects/ui/pages/ProjectList.html

Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped.

Create the parent directories, then save this complete file.

```html
<template><section><h1>Projects</h1><p>This screen is built in the next UI lesson.</p></section></template>
<script lang="ts">export default {};</script>
<style scoped>
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
```

**Say:** "Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped."

**Type:** Follow the action above in `modules/projects/ui/pages/ProjectList.html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 7: CREATE - make parent folders, then create this file - modules/projects/ui/pages/ProjectDetails/[id].html

Replace the placeholder with the project detail page. Load the owned project and its tasks using the router ID, then compose the task form, list and progress display.

Create the parent directories, then save this complete file.

```html
<template><section><h1>Project details</h1><p>This screen is built in the next UI lesson.</p></section></template>
<script lang="ts">export default {props: ['id']};</script>
<style scoped>
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
```

**Say:** "Replace the placeholder with the project detail page. Load the owned project and its tasks using the router ID, then compose the task form, list and progress display."

**Type:** Follow the action above in `modules/projects/ui/pages/ProjectDetails/[id].html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 8: EDIT - replace the marked blocks - app.py

The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact.

### Edit 1: find this exact block

Open `app.py`. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.

```python
    if app.is_management:
        return app


    @app.get("/health/course")
    async def health():
```
### Replace that block with

```python
    app.use_teloce(
        project_root=ROOT,
        ui_dir="ui",
        title="Project Manager",
        options={"minifier": "minifyjs"},
    )
    if app.is_management:
        return app

    @app.get("/health/course")
    async def health():
```

Leave the rest of the file unchanged.

### Edit 2: find this exact block

Open `app.py`. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.

```python
        await User.all().limit(1)
        return {"data": {"status": "ok"}}

    @app.get("/")
    async def home():
        return {"data": {"message": "Backend lesson ready. The SPA starts in chapter 8."}}

    return app
```
### Replace that block with

```python
        await User.all().limit(1)
        return {"data": {"status": "ok"}}

    # Explicit shell routes preserve real 404 responses for unknown API paths.
    @app.get("/")
    @app.get("/login")
    @app.get("/projects")
    async def spa(request: Request, project_id=None, slug=None):
        return await request.compile("app.html", {})

    return app
```

Leave the rest of the file unchanged.

**Say:** "The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact."

**Type:** Follow the action above in `app.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Remove public/app.css

The replacement factory and components no longer load this generated starter stylesheet. Remove the file after replacing the shell.

## Run and check

```bash
python management.py runserver
# Open http://127.0.0.1:8000/
# Account/project screens are placeholders until chapter 9.
```

## Expected result

The shell and navigation appear at /. The account and project placeholder pages mount. /api/welcome/status still responds as JSON.

## Common errors

A blank page can be a compiler/import error: inspect terminal and browser console. Delete public/app.css after replacing the factory; the completed app does not mount it. The shell's explicit global body reset is intentional; child components own their other rules.

## Short exercise

Change the shell's scoped header rules without modifying a child component's styles.

## What to say and show

Say: "Serve a browser shell and placeholder pages from the existing backend. The server remains responsible for persistence and authorization; the browser presents the result."

Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.

## Save your checkpoint

```bash
git add .
git commit -m "Complete chapter 08: Teloce HTML shell and scoped CSS"
```
