# Customize Fresh UI

**Goal:** make the scaffolded frontend yours — edit a route, add an interactive island, restyle a component, install more UI primitives, and re-theme the app — without fighting the framework. NetScript scaffolds a real [Fresh](https://fresh.deno.dev) 2 application at `apps/dashboard/`, powered by the [`@netscript/fresh`](https://rickylabs.github.io/netscript/reference/fresh/) meta-framework, and the UI components live in *your* repository as copied source. After the copy, that code is yours to change.

This is a task-oriented recipe. It assumes you already have a NetScript workspace (created with `netscript init`) whose `apps/dashboard/` app type-checks and runs. For the full generated API of the building blocks, follow the reference links at the end — this page never duplicates the [reference](https://rickylabs.github.io/netscript/reference/fresh-ui/).

> The ownership model in one sentence
>
> The Fresh runtime (
>
> @netscript/fresh
>
> ) and the UI registry (
>
> @netscript/fresh-ui
>
> ) are framework packages, but every component you render is
>
> copied into `apps/dashboard/components/ui/`
>
> by
>
> netscript ui:init
>
> /
>
> ui:add
>
> — so editing the UI is editing your own files, not patching a dependency.

## Before you start

You need:

- An existing NetScript workspace with the scaffolded `apps/dashboard/` Fresh app. If you do not have one yet, create it first (`netscript init`) — see the [Quickstart](https://rickylabs.github.io/netscript/quickstart/) or the [Storefront tutorial](https://rickylabs.github.io/netscript/tutorials/storefront/).
- The `netscript` command on your path. Run `netscript --help` to confirm, and `netscript ui:init --help` / `netscript ui:add --help` for the exact option spelling in your installed version. If `netscript` is not found, install it with `deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11`.

Run the app while you work so you can see each change live. Aspire is step 2 of the normal startup flow — it brings up your database (Postgres is the recommended engine; or `mysql` / `mssql` / `sqlite` chosen at scaffold time via `--db`) and Redis before any `netscript db` command and orchestrates the dashboard for you. You can also run the Fresh app on its own when you only need the UI loop:

```bash
# Brings up Postgres + Redis AND the dashboard; Aspire dashboard graph at https://localhost:18888
cd aspire && aspire start
```

```bash
# Leaner single-process loop — Vite dev server with HMR
deno task --cwd apps/dashboard dev
```

> Start at /design
>
> Before changing anything, open the scaffolded
>
> /design
>
> showcase in the running app. It renders the live component gallery (
>
> /design/components
>
> ), the token reference (
>
> /design/tokens
>
> ), and composition rules (
>
> /design/composition
>
> ) against the active theme — all from the app-owned copies, so it is the fastest way to feel what you already have.

## Where the app lives

Everything for the frontend is under `apps/dashboard/`. The layout follows Fresh 2 file-system routing, with NetScript conventions layered on top:

**apps/dashboard/ — what each path owns**

| Name | Type | Description |
| --- | --- | --- |
| `main.ts` | `app entry` | Bootstraps the Fresh app with defineFreshApp from @netscript/fresh/server. Reads PORT and prints the startup banner you see in aspire start logs. |
| `routes/` | `pages + layouts` | File-system routes. index.tsx, dashboard.tsx, health.tsx, examples/, plus _app.tsx (HTML shell) and _layout.tsx (chrome). Groups like (_components)/, (_shared)/, and (_islands)/ are non-routing co-location folders. |
| `islands/` | `client interactivity` | Hydrated Preact components (ThemeToggle, SidebarToggle, Toast). Everything else renders on the server only. |
| `components/ui/` | `app-owned UI` | The copied @netscript/fresh-ui primitives — Button, Card, Badge, PageHeader, StatsGrid, and friends. Edit these freely; they are yours after the copy. Barrel at components/ui/mod.ts. |
| `assets/` | `styling + tokens` | tokens.css / tokens.json (the --ns-* design tokens), styles.css, design.css, and per-component CSS under assets/ui/. Tailwind v4 is wired through Vite. |
| `lib/` | `app helpers` | cn.ts (class merge), example-service.ts, public-types.ts — non-UI utilities the routes import. |
| `router.ts / utils.ts` | `typed wiring` | router.ts exposes typed route references (appRoutes); utils.ts exports the createDefine() define helper and the definePage() builder. |
| `vite.config.ts / deno.json` | `build + deps` | Vite + Fresh + Tailwind plugins and the @app/* aliases; deno.json pins fresh, preact, @preact/signals, tailwindcss, and the @netscript/fresh* imports. |

The app entry is one call — `defineFreshApp` keeps the static-file and filesystem-route bootstrap framework-managed, so you only author routes, islands, and components:

```ts
import { defineFreshApp } from '@netscript/fresh/server';
import type { State } from '@app/utils.ts';

export const app = defineFreshApp<State>({ name: 'dashboard' });

// PORT is supplied by Aspire; defaults locally. The banner shows in `aspire start` logs.
const port = parseInt(Deno.env.get('PORT') || '8010');
console.log(`[dashboard] listening on http://localhost:${port}`);
```

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source of truth is your `netscript.config.ts` `apps.<name>.port` field, which the scaffold wires as the fallback default — set the port there rather than editing this line.

> Two import roots, two jobs
>
> @netscript/fresh
>
> is the
>
> runtime meta-framework
>
> : it ships the app bootstrap (
>
> /server
>
> ), route and page builders, and a typed data layer (
>
> /query
>
> ), alongside
>
> /builders
>
> ,
>
> /route
>
> ,
>
> /form
>
> ,
>
> /defer
>
> , and
>
> /interactive
>
> subpaths.
>
> @netscript/fresh-ui
>
> is the
>
> component registry
>
> the CLI copies from. You import the runtime directly; you import UI from your own
>
> @app/components/ui/mod.ts
>
> barrel, not from the registry package.

> Meta-framework vs scaffolded app
>
> Keep two ideas distinct:
>
> @netscript/fresh
>
> is the reusable
>
> meta-framework
>
> (server/islands/query primitives that any Fresh app can import), while
>
> apps/dashboard/
>
> is
>
> your scaffolded instance
>
> of it. The concept hub on
>
> Fresh UI
>
> explains how the two relate and where each layer's responsibilities begin and end.

## Edit a route

Create a typed page first with `netscript ui:add page status --route status --island`, then customize its generated view. This writes `routes/status/index.tsx` with `definePage()`; `--island` also creates colocated `(_islands)/` and `(_shared)/query-loaders.ts` seams. The Vite route generator discovers it on the next build.

Routes are plain `.tsx` files under `routes/`. The scaffold uses the `definePage()` builder (from `@app/utils.ts`) so a page declares its route, metadata, and the view it renders in one typed chain. To change the home page, edit `routes/(_components)/home-view.tsx` (the presentational view) and/or `routes/index.tsx` (the page declaration and its data):

```tsx
import HomeView from './(_components)/home-view.tsx';
import { appRoutes } from '@app/router.ts';
import { definePage } from '@app/utils.ts';

export const homePage = definePage()
  .withRoute(appRoutes.home)
  .withMeta(() => ({ title: 'my-app — dashboard', description: 'My app overview.' }))
  .withLayer('home', HomeView, () => ({
    projectName: 'my-app',
    appName: 'dashboard',
    routes: [
      { title: 'Dashboard', href: appRoutes.dashboard.href(), description: 'Operational overview.', cta: 'Open', badge: 'app' },
    ],
  }))
  .withLayout((slots) => slots.home())
  .build();

export const { default: page } = homePage;
export { page as default };
```

```tsx
// routes/status.tsx — a new route at /status
import { definePage } from '@app/utils.ts';
import { appRoutes } from '@app/router.ts';

export const statusPage = definePage()
  .withRoute(appRoutes.home) // swap for a typed route once you add it to router.ts
  .withMeta(() => ({ title: 'Status' }))
  .withLayer('status', () => <main class='ns-shell ns-section'>All systems go.</main>, () => ({}))
  .withLayout((slots) => slots.status())
  .build();

export const { default: page } = statusPage;
export { page as default };
```

The HTML shell (`<html>`, `<head>`, fonts, the theme-seed script) is `routes/_app.tsx`; the top bar / navigation chrome wrapping content pages is `routes/_layout.tsx`. Both use the `define.page(...)` / `define.layout(...)` helpers from `createDefine<State>()`. Edit those to change the global frame rather than repeating markup per page.

> Co-location folders don't become URLs
>
> Folders wrapped in parentheses —
>
> (_components)
>
> ,
>
> (_shared)
>
> ,
>
> (_islands)
>
> ,
>
> (design)
>
> — are Fresh
>
> route groups
>
> : they organize files next to the route that uses them without adding a path segment. Keep a page's view, loader, and demo islands beside the route file and the URL stays clean.

> Add typed route references in router.ts
>
> When you add a permanent page, register it in
>
> apps/dashboard/router.ts
>
> so
>
> appRoutes
>
> exposes a typed
>
> .href()
>
> for it. Linking through
>
> appRoutes
>
> instead of hand-written strings means a renamed or removed route fails the type-check rather than 404-ing at runtime.

## Add interactivity with an island

Generate a standalone signals island with `netscript ui:add island Counter`. Add `--query` for a `QueryIsland` provider wired to `@netscript/fresh/query`, then import the generated island from the route where it should hydrate.

Server-rendered routes ship zero client JavaScript. When you need state in the browser — a toggle, a form, a live panel — add an **island** under `islands/`. Islands are the only components Fresh hydrates on the client. The scaffold ships `ThemeToggle`, `SidebarToggle`, and `Toast` under `islands/ui/` as working examples; model new ones on them using Preact signals:

```tsx
import { useSignal } from '@preact/signals';
import type { VNode } from 'preact';

const Counter = (): VNode => {
  const count = useSignal(0);
  const increment = () => { count.value += 1; };
  return (
    <button type='button' onClick={increment} class='ns-button'>
      Clicked {count.value} times
    </button>
  );
};

export default Counter;
```

```tsx
// routes/(_components)/home-view.tsx
import Counter from '@app/islands/Counter.tsx';

const HomeView = () => {
  return (
    <main class='ns-shell ns-section'>
      {/* Rendered on the server, hydrated on the client */}
      <Counter />
    </main>
  );
};

export default HomeView;
```

A few rules keep islands cheap and correct: keep them small and leaf-shaped; pass plain serializable props in (the island boundary is the hydration boundary); and declare interactive event handlers with arrow functions, as in the scaffolded `ThemeToggle`. Anything that does not need browser state should stay a plain component in `components/` so it ships no JS.

> Fetch typed data with @netscript/fresh/query
>
> When an island or route needs server data, prefer the runtime's typed query layer (
>
> @netscript/fresh/query
>
> ) over hand-rolled
>
> fetch
>
> calls — it carries the oRPC contract types through to the client so a renamed service field surfaces as a type error. See
>
> `@netscript/fresh`
>
> for the query builders, and
>
> Add a service
>
> for the backend half of that contract.

## Restyle: tokens, Tailwind, and component CSS

NetScript styling has three layers, lightest-touch first:

1 · Design tokens (theme-wide)

Edit the CSS custom properties in assets/tokens.css (mirrored in tokens.json). Tokens are named --ns-* (e.g. surface, border, foreground, accent) and drive every component plus the light/dark themes selected by the data-theme attribute. Change a token once and the whole app re-themes.

2 · Tailwind utilities (per element)

Tailwind v4 is wired through Vite (@tailwindcss/vite). Use utility classes in JSX for one-off layout and spacing. The scaffold also defines NetScript layout helpers (ns-shell, ns-section, ns-stack, ns-cluster) in assets/layouts.css for consistent page rhythm.

3 · Component CSS (one primitive)

Each copied primitive has its own stylesheet under assets/ui/ (button.css, card.css, badge.css, …). To restyle just one component everywhere, edit its file there — it is app-owned source, imported by assets/styles.css.

Pick the lightest layer that does the job: reach for a **token** when the change should re-theme the whole app, a **Tailwind utility** for one-off element layout, and **component CSS** when one primitive should look different everywhere it appears.

Theme switching is already wired: `routes/_app.tsx` seeds `data-theme` from the `ns-theme` localStorage key (falling back to the OS preference), and the `ThemeToggle` island flips it at runtime. To ship a different default, change the `data-theme` attribute on `<html>` in `_app.tsx` and adjust the token values for that theme in `assets/tokens.css`.

> Edit tokens, not magic numbers
>
> Reach for a raw hex color or pixel value only when no token fits. Hard-coded values drift away from the theme and break the light/dark switch — the whole point of the
>
> --ns-*
>
> tokens is that one edit re-themes every surface consistently.

## Install more UI from the registry

When you need a primitive the scaffold didn't copy in, pull it from the `@netscript/fresh-ui` registry with the CLI. Two commands do the work, and both **copy source files into your app** rather than adding a runtime dependency:

**Fresh UI CLI commands**

| Name | Type | Description |
| --- | --- | --- |
| `netscript ui:init` | `install the foundation` | Installs the NetScript Fresh UI foundation set into an app workspace. Run once when setting up UI in an app that doesn't have it yet (the scaffold runs the equivalent for you). |
| `netscript ui:add ` | `add one item or collection` | Copies a single registry item or a named collection into the app workspace — component files go to apps/dashboard/components/ui/, island files to islands/ui/, lib helpers to lib/, and assets to assets/ui/ — then wires the CSS and merges any required deno.json imports. |

```bash
# Add one component (or a named collection) to the dashboard app
netscript ui:add dropzone

# Add an AI / workspace primitive — e.g. the chat message renderer
netscript ui:add message

# Add a whole named collection in one call (theme + all its items)
netscript ui:add ai
```

### Available components

Every item below is something you can `ui:add <name>` by its registry id. They fall into four groups. **Interactive namespaces** ship from the `@netscript/fresh-ui/interactive` sub-path (stateful, accessible compound components); the rest are copy-source registry items.

**Interactive namespaces (@netscript/fresh-ui/interactive)**

| Name | Type | Description |
| --- | --- | --- |
| `Accordion` | `disclosure` | Compound accordion with root + item subcomponents. |
| `Dialog` | `overlay` | Modal dialog with structural subcomponents. |
| `Drawer` | `overlay` | Edge-docked drawer with structural subcomponents. |
| `Popover` | `floating` | Anchored popover with positioning subcomponents. |
| `Sheet` | `overlay` | Side-docked inspection panel. |
| `Tabs` | `navigation` | Tabs with list, trigger, and content subcomponents. |
| `Tooltip` | `floating` | Anchored tooltip with positioning subcomponents. |
| `Combobox` | `headless L1` | Headless combobox seam (useCombobox + getRootProps/getInputProps/getContentProps/getItemProps) — the keyboard/selection engine behind the command palette and autocomplete inputs. |

**L0 primitives (@netscript/fresh-ui/primitives)**

| Name | Type | Description |
| --- | --- | --- |
| `Show` | `control flow` | Conditionally renders children without an extra DOM wrapper. |
| `VisuallyHidden` | `a11y` | Renders content for assistive tech while keeping it visually hidden. |
| `SrOnly` | `a11y` | Screen-reader-named alias for VisuallyHidden. |

**AI / workspace primitives (ui:add )**

| Name | Type | Description |
| --- | --- | --- |
| `avatar` | `identity` | Identity chip for a person or agent — initials or image with size, presence, and agent variants. |
| `citation-chip` | `ai` | Inline per-claim source marker [n] that pairs with a sources list — the grounded-agent citation UX. |
| `code-block` | `ai` | Fenced code surface with filename/language header and a copy affordance for assistant messages. |
| `model-selector` | `ai` | Disclosure-backed model/provider picker for the prompt composer (native details). |
| `tool-call-card` | `ai` | Inline MCP/tool invocation + result as a native details disclosure with a status badge and IO panel. |
| `chart-block` | `analytics` | Inline token-driven metric chart — horizontal bars or a vertical column chart with y-axis ticks and data-tone intents. |
| `donut` | `analytics` | Token-driven donut/pie chart — SVG arc segments with a center total and legend. |
| `prompt-input` | `ai composer` | Chat composer: a CSS auto-grow textarea (native field-sizing: content, no JS handler) with a toolbar of research/grounding pills, model picker, attach/screenshot/voice, and send. |
| `message` | `ai chat` | Chat message with author/time, inline-markup body (bold/code/[n] citations), tool-call + chart/code blocks, follow-up chips, and a typing indicator. Exports renderInline + TypingIndicator. |
| `dropzone` | `upload` | File-ingest affordance — a dashed drop target with built-in drag-drop, clipboard-paste, and file-picker ingest; filters by accept / multiple and reports results through onFile / onFiles / onReject. |

**Command & search utilities (ui:add )**

| Name | Type | Description |
| --- | --- | --- |
| `command-palette` | `palette` | Modal ⌘K command palette (.ns-cmdk) — the L1 Dialog backdrop/overlay wrapping the L1 Combobox: grouped, searchable commands with icon/hash/kind sub-parts. |
| `search` | `navigation` | Compact nav search affordance (.ns-search) — a button styled as an input with a ⌘K hint that opens the command palette. |

> Collections copy a whole set at once
>
> Named
>
> collections
>
> are also valid
>
> ui:add
>
> targets. For example
>
> netscript ui:add ai
>
> installs the AI surface set (citation-chip, model-selector, tool-call-card, prompt-input, message, command-palette, search) with their theme seed in one command;
>
> foundation
>
> installs the full dashboard set. See the
>
> reference
>
> for the complete item and collection catalog.

Both commands accept the same useful flags (run `--help` for the version-accurate list):

- `--project-root <path>` — target a workspace other than the current directory.
- `--theme <name>` — install against a specific theme registry item instead of the default official theme.
- `--registry-root <path>` — override the Fresh UI package root (advanced/local development).
- `--force` — overwrite existing copied UI files when re-running.

After the copy, the new component is regular source under `components/ui/` (and its styles under `assets/ui/`). Import it through the `@app/components/ui/mod.ts` barrel like the rest, then edit it however you like — there is no upstream patch to keep in sync.

> Copy-source, not a dependency
>
> This is the deliberate ownership tradeoff:
>
> ui:add
>
> gives you the code, so you carry it. When the registry ships an upstream fix you want, re-run
>
> ui:add … --force
>
> to re-copy — but expect to re-apply any local edits, exactly as you would with any vendored source.

## Verify your changes

With the app running, confirm the loop end to end:

1. Watch the Vite dev server (or `aspire start` logs) recompile on save — Fresh hot module replacement updates the page without a full reload.
2. Open `/design/components` to see restyled primitives render against the active theme, and `/design/tokens` to confirm token edits took effect.
3. Toggle the theme (the `ThemeToggle` in the top bar) to check both light and dark look right after token changes.
4. Type-check the app before committing:

```bash
deno task --cwd apps/dashboard check
```

That task runs `deno fmt --check`, `deno lint`, and `deno check` over the app, so a clean run means your routes, islands, and edited components still type and lint.

> Type-check is your guardrail
>
> Because routes, islands, and UI primitives are all app-owned TypeScript, the
>
> check
>
> task is the single gate that proves a route rename, a changed island prop, or an edited primitive still composes. Run it before every commit — green here means the Fresh build will not break on a missing import or a drifted type.

## Next steps

- Capability hub: [Fresh UI](https://rickylabs.github.io/netscript/capabilities/fresh-ui/) — the concept, the headline API, and the Learn / Do / Reference triplet for the dashboard app.
- Reference: the generated API for the UI registry and the Fresh runtime — [`@netscript/fresh-ui`](https://rickylabs.github.io/netscript/reference/fresh-ui/) and [`@netscript/fresh`](https://rickylabs.github.io/netscript/reference/fresh/). These are the authority for every export (the `/server`, `/query`, and sibling subpaths included); this guide never duplicates them.
- Related recipes: [Add a service](https://rickylabs.github.io/netscript/services-sdk/how-to/add-a-service/) to give your UI a typed oRPC backend, and [Add OpenTelemetry](https://rickylabs.github.io/netscript/observability/how-to/add-opentelemetry/) to trace it.
- Concepts: the [contracts](https://rickylabs.github.io/netscript/explanation/contracts/) explanation shows how a typed contract flows from service to client to island.

[Add OpenTelemetry](https://rickylabs.github.io/netscript/netscript/observability/how-to/add-opentelemetry/) [Build a durable chat](https://rickylabs.github.io/netscript/netscript/ai/how-to/build-a-durable-chat/)
