For Team

Add a JSX app

A "JSX app" is a single-file React component in apps/ that gets auto-wrapped into a standalone HTML page by deploy.sh. They're how every interactive tool on the intranet is built (Recruiting, Dashboards, Team Calendar, etc.).

The recipe

  1. Create a file at apps/<name>.jsx.
  2. Default-export a function component.
  3. Push.
git add apps/my_new_tool.jsx
git commit -m "Add my new tool"
git push origin main

Within 5 minutes:

You also probably want to add it to the homepage tool grid in index.html (see below).

The file shape

import { useState } from "react";

export default function MyNewTool() {
  const [count, setCount] = useState(0);
  return (
    <div style={{ padding: 20, fontFamily: 'system-ui' }}>
      <h1>Hello, intranet</h1>
      <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
    </div>
  );
}

That's it. No index.html, no build config, no router, no package.json.

What you get for free

The wrapper script pre-destructures these from React so you don't have to import them:

const { useState, useEffect, useRef, useCallback, useMemo } = React;

Use any of these directly without import statements. The wrapper strips your import line.

What you cannot import

Only import { useState } from "react" is supported (and it gets stripped — the React hooks are provided by the wrapper). You cannot import:

If you need a utility, define it in the same file. If you need a stylesheet, embed it in a <style> tag inside your component or use inline style={{}} props.

If you genuinely need a more complex frontend (multiple files, a real bundle), look at how Laurelin does it: apps/laurelin.entry.jsx is an esbuild entrypoint that pulls in apps/laurelin.jsx plus its dependencies. You'd add a similar entrypoint and a corresponding esbuild line in deploy.sh. But for almost all apps, single-file is correct.

Hitting the API

If you need data, fetch it. The intranet's Node backend exposes Laurelin endpoints at /api/laurelin/* and Recruiting at /api/recruiting/*.

export default function CompaniesView() {
  const [companies, setCompanies] = useState([]);
  useEffect(() => {
    fetch("/api/laurelin/companies")
      .then((r) => r.json())
      .then(setCompanies);
  }, []);
  return (
    <ul>{companies.map((c) => <li key={c.id}>{c.name}</li>)}</ul>
  );
}

The full API surface: API reference.

Naming

snake_case.jsx. Examples in the repo:

apps/company_intake.jsx
apps/content_library.jsx
apps/dd_matrix.jsx
apps/data_ops.jsx
apps/team_vacation_calendar.jsx
apps/portfolio_reports.jsx

Filename becomes the URL slug: apps/team_vacation_calendar.jsx/apps/team_vacation_calendar.html.

Adding to the homepage

The homepage card grid is hardcoded in index.html. Find the existing pattern (around line 217):

<a href="/apps/my_new_tool.html" class="tool-card">
    <span class="tool-badge jsx">JSX</span>
    <div class="tool-title">My New Tool</div>
    <div class="tool-desc">Short, plain-English description of what it does.</div>
</a>

Available badges:

Badge When to use
JSX Standard interactive app — most things
Draft Preview / not-yet-wired-up
Map Diagrams, product flows, architecture visuals
HTML A static HTML page rather than a JSX app

Commit both files together:

git add apps/my_new_tool.jsx index.html
git commit -m "Add My New Tool"
git push

Styling

Two approaches, pick one:

Inline styles (most common in our apps):

<div style={{
  padding: '24px',
  background: '#f4f7fa',
  border: '1px solid #c5cdd3',
  borderRadius: '8px',
}}>

Embedded stylesheet in a <style> block inside the component:

return (
  <>
    <style>{`
      .my-card { padding: 24px; background: #f4f7fa; }
      .my-card h2 { color: #324654; }
    `}</style>
    <div className="my-card">...</div>
  </>
);

Either works. Inline is faster to iterate on for small apps. Embedded stylesheet wins once you have repeated visuals.

The brand palette is in CLAUDE.md and in the Add a partnership review guide:

--background: #ffffff
--text:       #1c1c1b
--muted:      #92a8b6
--accent:     #324654   (ocean)
--surface:    #f4f7fa
--border:     #c5cdd3
--tinted:     #e6effa   (sky)

Common mistakes

Adding data files

If your app needs JSON data (rubrics, library content, configurations):

apps/rubrics/             ← copied as-is to /var/www/team-site/apps/rubrics/
apps/library/             ← copied as-is to /var/www/team-site/apps/library/
apps/proposals/           ← PDFs, copied as-is

Fetch from your app:

useEffect(() => {
  fetch("/apps/rubrics/audit-partners.json").then(r => r.json()).then(setRubric);
}, []);

Add new sub-directories under apps/ only if you have a reason — three is enough so far.

Iterating locally

There's no local dev server for the intranet — the deploy pipeline is the build. The two options:

Option A — preview in-browser by serving locally.

cd valinor-intra
python -m http.server 8000
# Open http://localhost:8000/apps/my_new_tool.jsx … no, won't work because Babel needs the HTML wrapper.

Instead, copy a wrapper template (from any deployed .html in /var/www/team-site/apps/) and adjust the inline script tag to point at your local file.

Option B — push to a feature branch and ssh to ValinorPC for a one-off deploy.

Cleaner, but slower turnaround. Most authors just push to main and accept the 5-minute deploy cycle, because Babel-standalone's runtime errors are visible in the browser console immediately.

When to graduate from a JSX app

If your tool becomes:

…it should probably move into the Laurelin app or get its own esbuild entry. Talk to engineering before adding a new top-level bundle — keeping the deploy script simple has been a deliberate choice and we don't add new bundle paths lightly.