The Outshift Design site — the design-research hub for Outshift by Cisco. It publishes research-backed frameworks, human–agent interaction (HAX) patterns, a developer-facing SDK reference, and a blog, all built as a statically-exported Next.js App Router site. Styling is primarily hand-written CSS driven by CSS custom properties, with light/dark theming and a small built-in chat assistant that helps visitors navigate the content.
Live demo: https://vaesposito.github.io/outshift-design-next/
ℹ️ The live URL above is a demo-only GitHub Pages deployment used for previewing this codebase. The real, production deployment of Outshift Design happens elsewhere — treat this Pages site purely as a sandbox/preview.
This README is written for both humans and the AI agents that help maintain the site. If you're adding or changing content, read the Theming, The asset() helper & the basePath gotcha, and Adding & modifying content sections carefully — the basePath rules in particular are easy to get wrong and cause broken links/images on the deployed site.
| Concern | Choice | Version (from package.json) |
|---|---|---|
| Framework | Next.js (App Router) | 16.2.7 |
| UI library | React + React DOM | 19.2.4 |
| Language | TypeScript | ^5 |
| Styling | Tailwind CSS | ^3.4.19 |
| CSS tooling | PostCSS + Autoprefixer | ^8.5.15 / ^10.5.0 |
| Theming | next-themes |
^0.4.6 |
| Node types | @types/node |
^20 |
- Next.js App Router with React 19 and TypeScript. The site is configured as a static export (
output: 'export') — there is no Node server at runtime. - Styling is primarily hand-written CSS using CSS custom properties. Global tokens and most layout live in
app/globals.css, and individual routes ship their own*.cssfiles (for exampleapp/sdk/sdk.css,app/blog/blog.css,app/research/research.css). Tailwind CSS is configured and available, and its color tokens are mapped to the CSS variables (seetailwind.config.ts), so a Tailwind class likebg-bgortext-text-mutedresolves to the same--color-*variable the hand-written CSS uses. In practice most components reach for hand-written classes; Tailwind is there when convenient. next-themespowers dark/light mode via adata-themeattribute on<html>.
- Node.js 20.x (the GitHub Actions deploy workflow uses Node 20; Next.js 16 requires a modern LTS Node).
- npm (a
package-lock.jsonis committed; usenpm cifor reproducible installs).
npm install
# or, for a clean, lockfile-exact install (what CI uses):
npm cinpm run devOpen http://localhost:3000. In development, basePath is empty, so URLs are served from the root.
npm run buildThis produces a fully static site in the out/ directory (output: 'export'). In a production build, basePath is set to /outshift-design-next, so the export is laid out to be served from that sub-path on GitHub Pages.
The
startscript (next start) exists from the create-next-app scaffold but is not used for this project — a static export has no server to start. To preview the built output, serve theout/folder with any static file server.
outshift-design-next/
├── app/ # Next.js App Router. Each route = a folder with a page.tsx.
│ ├── layout.tsx # Root layout: <html>, ThemeProvider, Header, Footer, ChatAssistant,
│ │ # inline theme bootstrap script, and basePath-aware CSS bg vars.
│ ├── globals.css # Global CSS variables (theming), base styles, shared layout.
│ ├── page.tsx # Home route ("/").
│ ├── favicon.ico
│ ├── hax/ # "/hax" — the HAX pattern library (HaxPatterns.tsx client component).
│ ├── guiding-principles/ # "/guiding-principles" (+ guiding-principles.css, PatternTabs.tsx).
│ ├── human-centered-ai-patterns/ # "/human-centered-ai-patterns" (+ its own .css).
│ ├── components/ # "/components" demo route (+ components.css).
│ ├── styleguide/ # "/styleguide" (+ styleguide.css).
│ ├── blog/ # "/blog" index (blog.css, BlogFilters.tsx = the article list/data).
│ │ ├── LightboxImage.tsx # Shared blog image → lightbox wrapper.
│ │ ├── augmented-designer/ # "/blog/augmented-designer" (page.tsx + article.css)
│ │ ├── making-room-for-agents/ # "/blog/making-room-for-agents"
│ │ ├── building-accessible-interfaces/
│ │ └── future-of-design-systems/ # Each article folder: page.tsx, article.css,
│ │ # and sometimes ArticleToc.tsx / ReadingProgress.tsx.
│ ├── research/ # "/research" hub (research.css) + sub-routes:
│ │ ├── foundational-principles/ # (+ PipelineStepper.tsx)
│ │ ├── cognitive-frameworks/
│ │ ├── security-privacy/
│ │ └── societal-impact/ # hub + agent-impact-map/, cognitive-load-audit/, foresight-canvas/
│ └── sdk/ # "/sdk" HAX SDK reference.
│ ├── layout.tsx # Wraps SDK routes with the sidebar + sdk.css.
│ ├── SdkSidebar.tsx # The SDK left-nav (SECTIONS array = source of truth for sidebar links).
│ ├── sdk.css # SDK-specific styling.
│ ├── page.tsx # "/sdk" intro + COMPONENT_CARDS grid.
│ └── <slug>/page.tsx # One folder per doc page (installation, code-editor, timeline, …).
├── components/ # Shared React components used across routes.
│ ├── Header.tsx # Top nav (uses <Link> + asset() for logos/icons).
│ ├── Footer.tsx
│ ├── ThemeToggle.tsx # next-themes light/dark toggle button.
│ ├── ChatAssistant.tsx # Floating chat widget (reads lib/chatKnowledge.ts).
│ ├── HeroVideo.tsx # Theme-aware <video> with light/dark sources via asset().
│ ├── ImageLightbox.tsx # Generic image lightbox/modal.
│ └── ReadingProgress.tsx # Rainbow scroll-progress bar for article pages.
├── lib/
│ ├── asset.ts # asset() helper + BASE_PATH (basePath-aware public asset URLs).
│ └── chatKnowledge.ts # KB[] knowledge base powering the ChatAssistant answers.
├── public/ # Served as-is. Referenced via asset('/...') (see basePath section).
│ ├── images/ # Logos, SVG backgrounds, blog/ and research/ and patterns/ images.
│ ├── videos/ # Hero/section .mp4 files (light + dark variants).
│ ├── files/ # Downloadable assets (e.g. ai-ethics-and-design.pdf).
│ ├── sdk.html # Legacy redirect to /sdk/ (see Deployment).
│ ├── .nojekyll # Tells GitHub Pages not to run Jekyll (preserves _next/ etc.).
│ ├── robots.txt, sitemap.xml
│ └── *.svg # Misc create-next-app leftovers (next.svg, vercel.svg, …).
├── .github/workflows/deploy.yml # GitHub Actions → GitHub Pages deploy.
├── next.config.ts # output:'export', basePath, assetPrefix, trailingSlash, images.unoptimized.
├── tailwind.config.ts # Tailwind color tokens mapped to CSS variables.
├── postcss.config.mjs # Tailwind + Autoprefixer.
├── tsconfig.json # "@/*" path alias → project root.
├── AGENTS.md / CLAUDE.md # Agent guidance (CLAUDE.md → @AGENTS.md).
└── README.md # You are here.
Key conventions
- A route is any folder under
app/containing apage.tsx. Nested folders create nested URLs. - Route-specific styling lives in a co-located
*.cssfile imported by that route'spage.tsx(orlayout.tsx). - Route-specific interactive pieces (filters, tabs, steppers, TOCs) are co-located client components (
'use client'), while truly shared UI lives in top-levelcomponents/. - The
@/import alias maps to the project root (seetsconfig.json), e.g.import { asset } from '@/lib/asset'.
Theming is a CSS-variable system toggled by a data-theme attribute on the <html> element, coordinated by next-themes.
- Light tokens are declared on
:rootinapp/globals.css; dark overrides are declared under[data-theme="dark"]:
/* app/globals.css */
:root {
--color-bg: #fff;
--color-text: #293247;
--color-accent: #00BCEB;
/* …more --color-* tokens… */
}
[data-theme="dark"] {
--color-bg: #0f1117;
--color-text: #e0e4ec;
--color-accent: #3fc6ee;
/* …dark overrides for the same tokens… */
}next-themesis configured inapp/layout.tsxwithattribute="data-theme",defaultTheme="system", andenableSystem. A tiny inlinethemeScriptruns before hydration to setdata-themefromlocalStorage(or the OS preference) and avoid a flash of the wrong theme.ThemeToggle.tsxflips betweenlightanddarkviauseTheme()fromnext-themes.- Theme-aware media: light/dark logos, images, and videos are swapped purely with CSS (
.logo-light/.logo-dark,video.video-light/video.video-dark, etc.).HeroVideo.tsxrenders both sources and shows the right one based on the resolved theme. - Decorative background SVGs are defined as
--bg-*variables. Because their URLs need the basePath, they're injected fromapp/layout.tsxusingasset()(see below) rather than hard-coded in CSS.
To change colors: edit the --color-* variables in app/globals.css (both the :root light set and the [data-theme="dark"] set). Because Tailwind's color tokens point at these variables (in tailwind.config.ts), updating the variables updates both the hand-written CSS and any Tailwind color utilities at once.
The site deploys to GitHub Pages under a sub-path: /outshift-design-next. In production, next.config.ts sets:
// next.config.ts
const nextConfig: NextConfig = {
output: 'export',
basePath, // '/outshift-design-next' in production, '' in dev
assetPrefix: basePath || undefined,
trailingSlash: true,
images: {
unoptimized: true,
},
// …
}Next.js applies basePath automatically to <Link href> and next/image — but NOT to raw <img>, <video>/<source>, or CSS url(). There are therefore two hard rules:
Use asset() from lib/asset.ts for any root-relative asset path in a raw <img>, <video>/<source>, inline style, or JS-referenced asset under public/:
// lib/asset.ts
export function asset(p: string): string {
const path = p.startsWith('/') ? p : `/${p}`
return `${BASE_PATH}${path}`
}import { asset } from '@/lib/asset'
// ✅ correct — resolves to /outshift-design-next/images/logo.svg in production
<img src={asset('/images/outshift_logo.svg')} alt="Outshift" />
// ❌ wrong — 404s on GitHub Pages (missing basePath)
<img src="/images/outshift_logo.svg" alt="Outshift" />Do not wrap
<Link href>ornext/imagesrcwithasset()— those already get the basePath and double-prefixing would break them.
Internal navigation must use the Next.js <Link> component. A plain <a href="/..."> does not get the basePath rewritten and will 404 on the deployed site.
import Link from 'next/link'
// ✅ correct — basePath is applied automatically
<Link href="/sdk/installation">Installation</Link>
// ❌ wrong — 404s on GitHub Pages
<a href="/sdk/installation">Installation</a>(In-page anchors like <a href="#section"> are fine — they don't involve the basePath. External links with full https:// URLs are also fine as plain <a>.)
Below are concrete, copy-pasteable recipes. After any change, run npm run dev to verify locally; the deploy happens automatically on push to main (see Deployment).
- Create a folder under
app/, e.g.app/my-page/. - Add
app/my-page/page.tsxexporting a default React component and (optionally)metadata:
import type { Metadata } from 'next'
import './my-page.css' // optional co-located styles
export const metadata: Metadata = {
title: 'My Page — Outshift Design',
description: 'What this page is about.',
}
export default function MyPage() {
return (
<main className="container">
<h1>My Page</h1>
</main>
)
}- (Optional) Add
app/my-page/my-page.cssand import it frompage.tsx. - Link to it from the nav (
components/Header.tsx) or other pages using<Link href="/my-page">.
Blog articles are App Router routes under app/blog/, and the blog index list/data lives in app/blog/BlogFilters.tsx (the CARDS array).
- Create
app/blog/<slug>/page.tsx(use an existing article likeapp/blog/making-room-for-agents/page.tsxas a template — they includemetadata, JSON-LD,import './article.css', and oftenReadingProgress). - Add a co-located
app/blog/<slug>/article.cssfor article-specific styling. - Put article images under
public/images/blog/<slug>/...and reference them withasset('/images/blog/<slug>/hero.png')(or via theLightboxImagewrapper for zoomable figures). - Register the card so the article appears on
/blog: add an entry to theCARDSarray inapp/blog/BlogFilters.tsx. For an internal article setexternal: falseandhref: '/blog/<slug>'; wrap image paths inasset(...).
SDK pages live under app/sdk/<slug>/ and share the sidebar from app/sdk/layout.tsx.
- Create
app/sdk/<slug>/page.tsxusing this template (note thesdk-main sdk-docclass on<main>):
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'HAX SDK — My Component',
description: 'What this component/doc page covers.',
}
export default function Page() {
return (
<main className="sdk-main sdk-doc">
<h1>My Component</h1>
<p>Description…</p>
<h2>Installation</h2>
<pre><code>{`hax init
hax add artifact my-component`}</code></pre>
</main>
)
}- Add a sidebar entry so it's reachable: add
{ label: 'My Component', href: '/sdk/my-component' }to the appropriate section in theSECTIONSarray inapp/sdk/SdkSidebar.tsx. - If it's a component, also add a card to the
COMPONENT_CARDSarray inapp/sdk/page.tsx(slug, image URL, title, description) so it shows up in the/sdkcomponents grid. - Note: SDK entry links from outside the SDK open in a new tab — when linking into the SDK from non-SDK pages, follow the existing pattern (target a new tab) rather than in-page
<Link>navigation.
- Drop files into
public/(e.g.public/images/...,public/videos/...,public/files/...). - Reference them through
asset('/path')in raw<img>/<video>/inline styles (see the basePath section). For theme-aware video use theHeroVideocomponent.
The floating ChatAssistant answers from a simple keyword-matched knowledge base in lib/chatKnowledge.ts (the exported KB array of { keys, answer } entries; findAnswer() picks the entry whose matched key is longest).
- To teach the assistant something new, add a
KBEntrytoKB: list triggerkeys(lowercase phrases) and ananswer(HTML string). - Use the existing
siteLink()/extLink()/haxNav()helpers to render internal links (already basePath-aware viaasset()), external links (open in a new tab), and HAX section links.
Deployment is automated via GitHub Actions → GitHub Pages (.github/workflows/deploy.yml).
- Trigger: every push to
main(also runnable manually viaworkflow_dispatch). - Pipeline: checkout → setup Node 20 (with npm cache) →
npm ci→npm run build→ upload theout/directory as a Pages artifact → deploy to thegithub-pagesenvironment. - Static export settings (
next.config.ts) that make this work:output: 'export'— emits a static site toout/(no server).basePath: '/outshift-design-next'(production only) +assetPrefix— so all routes/assets resolve under the Pages sub-path.trailingSlash: true— emitsroute/index.htmlfiles so directory-style URLs work on Pages.images: { unoptimized: true }— required because the Next.js image optimizer needs a server, which a static export doesn't have.
public/.nojekyllis included so GitHub Pages serves the export verbatim (Jekyll would otherwise ignore the_next/directory whose name starts with an underscore).- Legacy redirect:
output: 'export'doesn't supportredirects()/rewrites(), so the old/sdk.htmlURL is preserved by a staticpublic/sdk.htmlfile that meta-refreshes /location.replaces to/outshift-design-next/sdk/#introduction.
Pushing to main is all that's needed to deploy. This is a demo/preview deployment only — see the note near the top of this README.