Design and branding
Use Neatship's design system (tokens, components, icons, Storybook), light and dark themes, change the landing page's brand color and rename the product.
This guide shows how screens get their look, and how to make the starter yours: the design system you build with, light and dark themes, the landing page's brand color, and your product's name.
The design system: app-ui
packages/app-ui is Neatship's design system: a library of design tokens (named values for colors, spacing, font sizes, radius, shadows), components (buttons, inputs, dialogs, menus, toasts...) and icons. You own it: it started as a fork of an MIT-licensed open-source design system, and its LICENSE file must stay in the package.
Its look in short: almost everything is a neutral gray, and one blue means "you can act here". Body text is 13px. Spacing follows a 4px grid. Floating surfaces (menus, tooltips, toasts) are translucent with a blur. Every value has a designed light and dark version. The full description is in docs/conventions/06-design-system.md.
Import from the most specific entry point:
| Import path | What it contains |
|---|---|
app-ui/theme | Tokens (themeCssVariables), ThemeProvider, useTheme |
app-ui/icon | Icon components |
app-ui/primitives/<family> | Basic controls, grouped in families: input, surfaces, data-display, feedback, layout, navigation, typography, accessibility |
app-ui/components | Building blocks made from primitives (menus, toasts...) |
app-ui/utilities | Hooks and helpers |
import { Button } from 'app-ui/primitives/input';
import { AlertDialog } from 'app-ui/primitives/surfaces';
import { useToast } from 'app-ui/components';
import { IconTrash } from 'app-ui/icon';The interactive controls are built on Base UI, a library that provides keyboard support and accessibility. The Button component, for example, accepts variant (solid, outline, soft, ghost), color (neutral, accent, danger, success) and size (sm, md).
Browse the components in Storybook
Storybook is a catalog where each component is shown in each of its states.
npx nx run app-ui:storybook:serve:dev # opens on http://localhost:6008
npx nx run app-ui:storybook:test # runs the story tests in a browser (needs Playwright's browsers)Every story runs an automatic accessibility check that fails on any violation, and a lint rule (app/no-storybook-a11y-disable) forbids switching it off.
After adding or removing a public component in app-ui, update the list in packages/app-ui/docs/module-ownership.json with npx tsx packages/app-ui/scripts/checkModuleOwnership.ts --write, then review the change. npx nx test app-ui fails when the list is out of date.
Style your own screens with tokens
The web app styles its components with Linaria (styles written in the component file, turned into plain CSS at build time) and takes every value from themeCssVariables:
import { styled } from '@linaria/react';
import { themeCssVariables } from 'app-ui/theme';
const StyledHeader = styled.div`
align-items: center;
border-bottom: 1px solid ${themeCssVariables.border.color.light};
display: flex;
gap: ${themeCssVariables.spacing[2]};
padding: 0 ${themeCssVariables.spacing[3]};
`;Each value is a CSS variable, such as var(--t-spacing-2). The light and dark values live in two generated stylesheets, so a component never needs to know which theme is active.
Lint enforces three habits:
- styled components are named
Styled<Something>; - CSS properties are sorted alphabetically (
npx nx lint app-front --configuration=fixsorts them for you); - no color is written by hand (no hex,
rgb()orhsl()): that is what makes dark mode work everywhere.
When JavaScript needs a real number (an icon size, for example), use useTheme().
Where tokens come from
The tokens are written in TypeScript in packages/app-ui/design-tokens/. A generator turns them into theme-light.css, theme-dark.css and themeCssVariables.ts in packages/app-ui/src/theme/. Never edit those generated files by hand.
npx nx generateTokens app-ui # regenerate the theme files after editing design-tokens/
npx nx run app-ui:generate:check # fails if the theme files are out of dateIcons
Icons come from Tabler Icons, through a curated list in app-ui/icon. Importing @tabler/icons-react directly is a lint error in the web app.
Each product concept has one canonical icon, listed in packages/app-ui/src/icon/icon-dictionary.md with when to use it and when not to. For example: workspace IconBuildingSkyscraper, members IconUsers, billing IconCreditCard, permissions IconLock. The Markdown file is generated from packages/app-ui/src/icon/constants/IconDictionary.ts with npx nx generateIconDictionary app-ui.
Icons are passed to components as components, not elements: Icon={IconPlus}.
Light and dark
- Every semantic token has its own light and dark value.
ThemeProviderputs alightordarkclass on the page's root element, and the matching CSS variables apply. - In the app, Settings → Appearance offers System, Light and Dark. The choice is saved in the browser (local storage). The command menu (Cmd/Ctrl + K) can switch it too.
- Clerk's sign-in screens read the same CSS variables, so they follow the theme.
- The landing page is light only, on purpose.
The landing page's brand color
The landing page (packages/app-website) wears one brand color, taken from one of the design system's color scales. It is set by one line, in packages/app-website/src/modules/brand-color/constants/BrandColorScale.ts:
export const BRAND_COLOR_SCALE: BrandColorScale = 'blue';The scales on offer are blue (the default, the app's own blue), cyan, turquoise, grass, tomato, red, bronze and gray. The type accepts only these names. A test checks, for each of them, that white text on the brand fill and brand-colored text on the white page keep a contrast ratio of at least 4.5 to 1 (the accessibility minimum for body text). Orange, yellow and amber are not offered because their readable shades fail that test or turn brown.
To try colors before you choose:
- Start the landing page:
npx nx start app-website, then openhttp://localhost:5190. - Click the "Brand color" button in the bottom-left corner.
- Pick a scale. The page recolors at once, and your pick is remembered in this browser.
- The palette shows the exact line to paste into
BrandColorScale.ts, with a copy button.
The picker only exists in development. The build and the published pages never contain it.
The app itself uses the design system's blue as its accent. It has no one-line switch like the landing page: changing it means editing the tokens in packages/app-ui/design-tokens/ (the accent steps are in accent.ts), regenerating the theme, and reviewing both themes in Storybook.
Rename the product
The product name is one constant, PRODUCT_NAME, in packages/app-shared/src/constants/ProductName.ts:
export const PRODUCT_NAME = 'Neatship';Change it, then rebuild the shared package: npx nx build app-shared --skip-nx-cache. The constant feeds:
- the web app's browser tab title, and its description for search engines and shared links ("The <name> web app.").
packages/app-front/index.htmlhas neither: the pluginproduct-name-in-htmlinpackages/app-front/vite.config.tsadds both when the web app is built or served, and that is where you change the description's wording; - the name above the sign-in card, and its first letter used as a small logo;
- the "about" panel on the app's home page;
- the landing page (header, footer, pricing, page titles);
- the names and ids of your Stripe products (
<name> Pro,<name>_pro) and of the customer portal configuration.
Warning: Rename the product before you run npx nx run app-server:billing:sync-catalog for the first time. The Stripe product ids contain the name: a rename afterwards creates new products and prices, and leaves the old products in Stripe.
A few places don't read the constant. Change them by hand:
| Place | Where |
|---|---|
| The name in Clerk's sign-in texts and emails | the application name in the Clerk dashboard |
| The business name on Stripe Checkout and the portal | Stripe Dashboard → Settings → Public details |
| The repository's description | README.md, and the first lines of CLAUDE.md |
The landing page content
packages/app-website holds the landing page, in English (/) and French (/fr/), prerendered so search engines and visitors without JavaScript get the full page. Its copy currently presents Neatship itself: replace it with your product's story.
- Each section has its own folder in
packages/app-website/src/modules/landing/(for examplehero,features,pricing,faq). Its words are in the section'sconstants/folder, written as Linguimsgmessages so they can be translated. - The price shown is in
pricing/constants/LicenseOffer.ts, and what the offer includes inpricing/constants/PricingContent.ts. - The figures (components, icons, lint rules, tests) are in
key-figures/constants/KeyFiguresContent.ts. Update them when your product changes. - The page title and description for search engines are in
packages/app-website/src/modules/seo/constants/SeoContent.ts. - Three things are this website's own and must become yours before going live: the contact address
CONTACT_EMAIL(packages/app-website/src/modules/landing/constants/ContactEmail.ts), the "Built by" line in the footer'sFooterContent.ts, and the legal pages (legal notice, privacy policy, terms of sale): one Markdown file per page and language inpackages/app-website/legal/, to replace with your company's, ideally checked by a lawyer.
The landing page reads four settings from packages/app-website/.env:
| Variable | Used for |
|---|---|
VITE_WEBSITE_URL | where the site is served: canonical links, language alternates, sitemap |
VITE_APP_URL | where the app runs; the "Live demo" links use it when VITE_DEMO_URL is empty |
VITE_DEMO_URL | a public demo, if it runs somewhere else |
VITE_PURCHASE_URL | the payment link; empty shows the button as "Available soon" |
npx nx start app-website # development server on http://localhost:5190
npx nx build app-website # prerendered site in packages/app-website/build
npx nx preview app-website # build, then serve it on http://localhost:5191
npx nx run app-website:lingui:extract # after changing copy
npx nx run app-website:lingui:compileAfter changing copy, translate the new French entries in packages/app-website/src/locales/fr.po (home page, header, footer) and packages/app-website/src/locales/feature-pages/fr.po (the feature pages); see Translations. yarn verify lists any entry still missing.
Host the website on Vercel instead
The production stack serves the website from your server (the website container). If you prefer Vercel, packages/app-website/vercel.json already holds the build settings and the same security headers. In Vercel, import your repository as a new project, then:
- Root Directory:
packages/app-website. - Environment variables:
VITE_WEBSITE_URL,VITE_APP_URL(orVITE_DEMO_URL) andVITE_PURCHASE_URL, with your real addresses, plusENABLE_EXPERIMENTAL_COREPACK=1(Vercel then installs the exact Yarn version of the repository). - Domains: add your website's domain to the project.
Every push to main then rebuilds the website. The website container still runs on your server, but nothing reaches it once WEBSITE_DOMAIN points at Vercel.