feat: scaffold outreach engine monorepo on the novelpad-desktop stack

Workspaces: config (copied), outreach-core (schema + actions/queries +
hard gates), outreach-ai (Gemini client + embeddings copies, profiler and
mission-fit-judge agent stubs), outreach-worker (DBOS executor with
nightly ingest + hourly expiry workflows), outreach-review (RR7 review
queue v0). Initial drizzle migration incl. pgvector extension.

Stack contract: Yarn 4.5.0 + Turbo, Node 22.16, Drizzle 0.44.6 +
pgvector, DBOS 4.17.6, @google/genai on Vertex, gemini-embedding-001
@1536, React Router v7. Files copied from novelpad-desktop carry
provenance headers @ 62c56b87.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-16 11:08:24 -04:00
commit 14200edb60
80 changed files with 11637 additions and 0 deletions

View File

@@ -0,0 +1 @@
# tsconfig

View File

@@ -0,0 +1,21 @@
{
"name": "@novelpad/config",
"packageManager": "yarn@4.5.0",
"dependencies": {
"@fontsource-variable/bricolage-grotesque": "^5.2.10",
"@fontsource-variable/lora": "^5.2.8",
"@fontsource-variable/outfit": "^5.2.8",
"@fontsource-variable/source-serif-4": "^5.2.9",
"@fontsource/courier-prime": "^5.2.8",
"@fontsource/dm-serif-display": "^5.2.8",
"@fontsource/inter": "^5.2.8",
"@fontsource/libre-baskerville": "^5.2.8"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.19",
"tailwindcss": "^3.4.13",
"tailwindcss-animate": "^1.0.7",
"vite": "^5.4.11"
}
}

710
packages/config/shadcn.css Normal file
View File

@@ -0,0 +1,710 @@
@import '@fontsource/inter';
@import '@fontsource-variable/bricolage-grotesque';
@import '@fontsource-variable/outfit';
@import '@fontsource-variable/source-serif-4';
@import '@fontsource-variable/lora';
@import '@fontsource/courier-prime';
@import '@fontsource/dm-serif-display';
/* Libre Baskerville — a user-selectable editor font (font-family-dropdown.tsx),
self-hosted so it keeps working offline after the Google-Fonts CDN removal. */
@import '@fontsource/libre-baskerville';
@font-face {
font-family: 'Fenwick';
src: url('/Fenwick-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
font-display: swap;
}
/* ── Dialog system surfaces (brand-agnostic; spec: docs/redesign/dialogs) ──
The dialog surface is intentionally NOT --background: it matches the elevated
floating-toolbar / popover color so modals read as the top layer. */
:root {
--np-dialog-surface: #ffffff;
--np-dialog-line: rgba(28, 26, 21, 0.1);
--np-scrim: rgba(28, 26, 21, 0.34);
--np-dialog-shadow: 0 32px 90px rgba(28, 26, 21, 0.24), 0 2px 8px rgba(28, 26, 21, 0.1);
/* Popover-tier elevation — lighter than the dialog shadow. Shared by
popover/dropdown/select content for a consistent floating-surface feel. */
--np-popover-shadow: 0 8px 24px rgba(28, 26, 21, 0.12), 0 2px 6px rgba(28, 26, 21, 0.08);
/* Brand accent tokens consumed by the dialog system (IconBadge gradient,
DialogNote/DialogField brand tone). Defaulted here so they always resolve
pre-hydration (before BrandingProvider sets data-brand) and for any
unbranded root; the per-brand blocks below override them. Default = NovelPad. */
--np-gradient: linear-gradient(135deg, #22D3EE 0%, #D946EF 55%, #EC4899 100%);
--np-gradient-soft: linear-gradient(135deg, rgba(34, 211, 238, .18) 0%, rgba(217, 70, 239, .18) 55%, rgba(236, 72, 153, .18) 100%);
--np-brand-ink: #D946EF;
--np-brand-soft: rgba(217, 70, 239, 0.13);
}
.dark {
--np-dialog-surface: #1b2238;
--np-dialog-line: rgba(255, 255, 255, 0.1);
--np-scrim: rgba(6, 9, 20, 0.64);
--np-dialog-shadow: 0 32px 90px rgba(0, 0, 0, 0.62), inset 0 0 0 1px rgba(255, 255, 255, 0.02);
--np-popover-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255, 255, 255, 0.02);
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--constructive: 142.1 70.6% 45.3%;
--constructive-foreground: 210 40% 98%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--highlight: 217 91% 60%;
--radius: 0.5rem;
/* Theme-aware radius scale. Values mirror the current Tailwind output so
introducing themes does not change today's NovelPad appearance. */
--radius-sm: 0.25rem;
--radius-xs: 0.1875rem;
--radius-base: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-compact: 0.5625rem;
--radius-icon: 0.625rem;
--radius-control: 0.6875rem;
--radius-tile: 0.75rem;
--radius-badge: 0.8125rem;
--radius-panel: 0.875rem;
--radius-dialog: 1.125rem;
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-serif: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-reading: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--app-header-background: hsl(var(--background));
--app-header-foreground: hsl(var(--foreground));
--app-header-border: hsl(var(--border));
--column: 210 40% 92.1%;
--column-foreground: 222.2 47.4% 11.2%;
--column-border: 210 40% 86.1%;
/* Board-specific surfaces default to today's muted canvas and floating
section stacks. Visual themes can override them independently without
changing muted UI throughout the rest of the application. */
--board-background: var(--muted);
--board-action-foreground: hsl(var(--foreground));
--board-column-gap: 1.5rem;
--board-padding-inline: 1.5rem;
--board-padding-block-start: 1.25rem;
--board-padding-block-end: 0rem;
--board-padding-block-end-stacked: 4rem;
--board-header-background: hsl(var(--background));
--board-header-foreground: hsl(var(--foreground));
--board-header-muted-background: hsl(var(--muted) / 0.5);
--board-header-muted-foreground: hsl(var(--muted-foreground));
--board-header-border: hsl(var(--column-border));
--board-header-border-width: 1px;
--board-header-control-border: hsl(var(--border));
--board-footer-background: hsl(var(--muted));
--board-footer-foreground: hsl(var(--muted-foreground));
--board-footer-muted-foreground: hsl(var(--muted-foreground) / 0.7);
--board-footer-hover-background: hsl(var(--accent));
--board-footer-border: hsl(var(--border));
--board-footer-border-width: 1px;
--section-column-background: transparent;
--section-column-radius: 0rem;
/* Recessed board canvas (ink-0): darker than --background (chrome). */
--canvas: 0 0% 100%;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 213 31% 91%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--constructive: 142.1 44.1% 37.1%;
--constructive-foreground: 210 40% 98%;
--destructive: 0 63% 42%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--highlight: 217 91% 60%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--column: 224 31% 22%;
--column-foreground: 213 31% 91%;
--column-border: 216 24% 30%;
--canvas: 224 71% 3%;
/* Raised card surface (lifts scene cards above the same-ink board in dark
mode). Brand blocks override this with their own on-brand value. */
--card-raised: 226 39% 15.5%;
}
}
:root[data-brand="helmdocs"] {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24.6 95% 53.1%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 24.6 95% 53.1%;
--highlight: 24.6 95% 53.1%;
--sidebar-background: 30 6% 97%;
--sidebar-foreground: 20 14.3% 4.1%;
--sidebar-primary: 24.6 95% 53.1%;
--sidebar-primary-foreground: 60 9.1% 97.8%;
--sidebar-accent: 60 4.8% 95.9%;
--sidebar-accent-foreground: 24 9.8% 10%;
--sidebar-border: 20 5.9% 90%;
--sidebar-ring: 24.6 95% 53.1%;
--column: 35 5% 92%;
--column-foreground: 20 14.3% 4.1%;
--column-border: 30 5% 85%;
--canvas: 0 0% 100%;
}
.dark[data-brand="helmdocs"] {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 20.5 90.2% 48.2%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 20.5 90.2% 48.2%;
--highlight: 20.5 90.2% 48.2%;
--sidebar-background: 20 10% 7%;
--sidebar-foreground: 60 9.1% 97.8%;
--sidebar-primary: 20.5 90.2% 48.2%;
--sidebar-primary-foreground: 60 9.1% 97.8%;
--sidebar-accent: 12 6.5% 15.1%;
--sidebar-accent-foreground: 60 9.1% 97.8%;
--sidebar-border: 12 6.5% 15.1%;
--sidebar-ring: 20.5 90.2% 48.2%;
--column: 15 10% 14%;
--column-foreground: 60 9.1% 97.8%;
--column-border: 12 8% 22%;
--canvas: 20 14.3% 3%;
/* HelmDocs cards already lift: --card (L 4.1%) is darker than the muted board
(L 15.1%), so the raised surface stays at --card — no NovelPad ink-navy. */
--card-raised: 20 14.3% 4.1%;
/* Dialog surface: a neutral raised dark panel at the --secondary level, NOT
the NovelPad ink-navy (#1b2238 from the .dark default). Keeps modals lifted
off the near-black background while staying on-brand. */
--np-dialog-surface: hsl(12 6.5% 15.1%);
--np-scrim: rgba(0, 0, 0, 0.64);
}
/* HelmDocs brand fonts (Outfit — geometric sans for body + headings;
theme-agnostic). Outfit has no serif companion, so --font-serif (used by
headings, e.g. the auth card title) also points at Outfit. Reading + mono
stay on the system/Georgia stacks. Variable face registers as
"Outfit Variable" — lead the stack with that or the self-hosted face never
loads. */
:root[data-brand="helmdocs"],
.dark[data-brand="helmdocs"] {
--font-sans: "Outfit Variable", "Outfit", ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
--font-serif: "Outfit Variable", "Outfit", ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
--font-reading: Georgia, "Times New Roman", serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--font-display: "Outfit Variable", "Outfit", ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
/* HelmDocs brand sweep + accent: the orange ramp (mirrors --loader-gradient
and --primary), replacing the NovelPad cyan→fuchsia gradient in shared
dialog chrome (IconBadge, DialogNote/Field brand tone, step dots, etc.). */
--np-gradient: linear-gradient(135deg, hsl(24.6 95% 53.1%) 0%, hsl(20.5 90.2% 48.2%) 100%);
--np-gradient-soft: linear-gradient(135deg, hsla(24.6, 95%, 53.1%, .18) 0%, hsla(20.5, 90.2%, 48.2%, .18) 100%);
--np-brand-ink: hsl(24.6 95% 53.1%);
--np-brand-soft: hsla(24.6, 95%, 53.1%, .14);
}
:root[data-brand="novelpad"] {
--background: 37.5 50% 96.9%;
--foreground: 42.9 14.3% 9.6%;
--muted: 40 38.5% 92.4%;
--muted-foreground: 32.7 6.9% 31.2%;
--popover: 37.5 50% 96.9%;
--popover-foreground: 42.9 14.3% 9.6%;
--border: 40 34.3% 86.3%;
--input: 40 34.3% 86.3%;
/* Card is the raised ink: lighter than the paper --background (L 96.9%) so it
lifts in light mode, mirroring dark mode where --card (L 12.9%) sits above
--background (L 10.6%). A darker card here reads as a recessed panel. */
--card: 38 60% 99%;
--card-foreground: 42.9 14.3% 9.6%;
--primary: 42.9 14.3% 9.6%;
--primary-foreground: 37.5 50% 96.9%;
--secondary: 40 38.5% 92.4%;
--secondary-foreground: 42.9 14.3% 9.6%;
--accent: 40 38.5% 92.4%;
--accent-foreground: 42.9 14.3% 9.6%;
--constructive: 160.1 84.1% 39.4%;
--constructive-foreground: 37.5 50% 96.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 37.5 50% 96.9%;
--ring: 188.7 94.5% 42.7%;
--highlight: 188.7 94.5% 42.7%;
--sidebar-background: 40 48.4% 93.9%;
--sidebar-foreground: 32.7 6.9% 31.2%;
--sidebar-primary: 188.7 94.5% 42.7%;
--sidebar-primary-foreground: 37.5 50% 96.9%;
--sidebar-accent: 40 38.5% 92.4%;
--sidebar-accent-foreground: 42.9 14.3% 9.6%;
--sidebar-border: 40 34.3% 86.3%;
--sidebar-ring: 188.7 94.5% 42.7%;
--column: 40 38.5% 92.4%;
--column-foreground: 42.9 14.3% 9.6%;
--column-border: 40 34.3% 86.3%;
/* Pro's status bar belongs to the board surface rather than reading as a
separate bordered panel. */
--board-footer-background: hsl(var(--board-background));
--board-footer-border: transparent;
--board-footer-border-width: 0px;
--canvas: 37.5 50% 96.9%;
}
.dark[data-brand="novelpad"] {
--background: 225 44.4% 10.6%;
--foreground: 216 33.3% 97.1%;
--muted: 226.2 39.4% 12.9%;
--muted-foreground: 221.5 17.6% 71%;
/* Menus/popovers sit at ink-3 (raised above the ink-2 cards), matching the
prototype's scene/stack/board menus. */
--popover: 224.1 36.2% 18.4%;
--popover-foreground: 216 33.3% 97.1%;
--border: 224.1 36.2% 18.4%;
--input: 223.9 32.8% 24.5%;
--card: 226.2 39.4% 12.9%;
--card-foreground: 216 33.3% 97.1%;
--primary: 216 33.3% 97.1%;
--primary-foreground: 225 44.4% 10.6%;
--secondary: 224.1 36.2% 18.4%;
--secondary-foreground: 216 33.3% 97.1%;
--accent: 224.1 36.2% 18.4%;
--accent-foreground: 216 33.3% 97.1%;
--constructive: 160.1 84.1% 39.4%;
--constructive-foreground: 216 33.3% 97.1%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 216 33.3% 97.1%;
--ring: 187.9 85.7% 53.3%;
--highlight: 187.9 85.7% 53.3%;
/* Sidebar = ink-1 (chrome level), matching the toolbar; the board canvas
(--canvas) is the darker ink-0 layer below it. */
--sidebar-background: 225 44.4% 10.6%;
--sidebar-foreground: 225 21.7% 82%;
--sidebar-primary: 187.9 85.7% 53.3%;
--sidebar-primary-foreground: 225 44.4% 10.6%;
--sidebar-accent: 224.1 36.2% 18.4%;
--sidebar-accent-foreground: 216 33.3% 97.1%;
--sidebar-border: 224.1 36.2% 18.4%;
--sidebar-ring: 187.9 85.7% 53.3%;
--column: 224.1 36.2% 18.4%;
--column-foreground: 216 33.3% 97.1%;
--column-border: 223.9 32.8% 24.5%;
/* Recessed board canvas = ink-0 (#0B1020), darker than --background (ink-1). */
--canvas: 225.7 48.8% 8.4%;
/* Raised card: ink-2 (--card, L 12.9%) lifted to L 15.5% so scene cards read
above the same-ink board (--muted). */
--card-raised: 226.2 39.4% 15.5%;
}
/* NovelPad brand fonts, gradient, tag ramp, motion + focus atmosphere (theme-agnostic) */
:root[data-brand="novelpad"],
.dark[data-brand="novelpad"] {
--font-sans: "Bricolage Grotesque Variable", "Bricolage Grotesque", ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif;
--font-serif: "Source Serif 4 Variable", "Source Serif 4", "Source Serif Pro", "Iowan Old Style", Georgia, serif;
--font-reading: "Lora Variable", "Lora", "Iowan Old Style", Georgia, serif;
--font-mono: "Courier Prime", "Courier New", ui-monospace, "SF Mono", Menlo, monospace;
--font-display: "Fenwick", "DM Serif Display", "Source Serif 4 Variable", "Source Serif 4", Georgia, serif;
--np-gradient: linear-gradient(135deg, #22D3EE 0%, #D946EF 55%, #EC4899 100%);
--np-gradient-soft: linear-gradient(135deg, rgba(34, 211, 238, .18) 0%, rgba(217, 70, 239, .18) 55%, rgba(236, 72, 153, .18) 100%);
--tag-cyan: #22D3EE;
--tag-pink: #EC4899;
--tag-plum: #7C3AED;
--tag-amber: #F59E0B;
--tag-sage: #5B8F6B;
--tag-dusk: #64748B;
--tag-coral: #FB7185;
--ease: cubic-bezier(0.32, 0.72, 0, 1);
--dur-fast: 150ms;
--dur-base: 220ms;
--dur-slow: 320ms;
--np-focus-ring: 0 0 0 2px hsl(var(--background)), 0 0 0 4px #22D3EE;
}
/* ── NovelPad Classic ──────────────────────────────────────────────────────
Visual theme for writers who prefer the previous NovelPad application.
Source palette: ../novelpad/src/novelpad/ux/theme.ts. The app's structural
layout stays current; semantic surfaces, accents, typography, and standard
radii reproduce the Classic character in both color modes. */
:root[data-brand="novelpad"][data-novel-theme="novelpad-classic"] {
--background: 60 11.1% 94.7%; /* #F3F3F0 */
--foreground: 247.1 11.4% 29.2%; /* #444253 */
--muted: 0 0% 93.3%; /* #EEEEEE */
--muted-foreground: 240 4% 34.7%; /* #55555C */
--popover: 0 0% 100%;
--popover-foreground: 247.1 11.4% 29.2%;
--border: 236.7 16.7% 78.8%; /* #C0C1D2 */
--input: 236.7 16.7% 78.8%;
--card: 0 0% 100%;
--card-foreground: 247.1 11.4% 29.2%;
--card-raised: 0 0% 100%;
--primary: 198.7 73.2% 49.8%; /* #22A2DC */
--primary-foreground: 253.3 20.9% 8.4%;
--secondary: 334.1 74.7% 52%; /* #E02978 */
--secondary-foreground: 0 0% 100%;
--accent: 236.7 16.7% 78.8%;
--accent-foreground: 244.6 11.5% 22.2%;
--constructive: 128.2 44.2% 44.3%; /* #3FA34D */
--constructive-foreground: 0 0% 100%;
--destructive: 6.2 81.7% 62.7%; /* #EE6352 */
--destructive-foreground: 253.3 20.9% 8.4%;
--ring: 198.7 73.2% 49.8%;
--highlight: 198.7 73.2% 49.8%;
/* Classic's navigation rail was dark in both modes. */
--sidebar-background: 244.6 11.5% 22.2%;
--sidebar-foreground: 210 21.4% 94.5%;
--sidebar-primary: 198.7 73.2% 49.8%;
--sidebar-primary-foreground: 253.3 20.9% 8.4%;
--sidebar-accent: 240 11.7% 15.1%;
--sidebar-accent-foreground: 210 21.4% 94.5%;
--sidebar-border: 244.6 11.5% 22.2%;
--sidebar-ring: 198.7 73.2% 49.8%;
--app-header-background: hsl(var(--sidebar-background));
--app-header-foreground: hsl(var(--sidebar-foreground));
--app-header-border: hsl(var(--sidebar-border));
--column: 0 0% 93.3%;
--column-foreground: 240 8.1% 21.8%;
--column-border: 236.7 16.7% 78.8%;
--board-background: 240 11.7% 15.1%; /* #22222B */
--board-action-foreground: #EEF1F4;
--canvas: 60 11.1% 94.7%;
--np-dialog-surface: #FFFFFF;
--np-dialog-line: rgba(68, 66, 83, 0.18);
--np-scrim: rgba(19, 17, 26, 0.38);
--np-dialog-shadow: 0 18px 48px rgba(34, 34, 43, 0.22), 0 2px 6px rgba(34, 34, 43, 0.10);
--np-popover-shadow: 0 8px 20px rgba(34, 34, 43, 0.18);
}
.dark[data-brand="novelpad"][data-novel-theme="novelpad-classic"] {
--background: 253.3 20.9% 8.4%; /* #13111A */
--foreground: 210 21.4% 94.5%; /* #EEF1F4 */
--muted: 244.6 11.5% 22.2%; /* #33323F */
--muted-foreground: 236.7 16.7% 78.8%;
--popover: 244.6 11.5% 22.2%;
--popover-foreground: 210 21.4% 94.5%;
--border: 247.1 11.4% 29.2%; /* #444253 */
--input: 240 4% 34.7%; /* #55555C */
--card: 240 11.7% 15.1%; /* #22222B */
--card-foreground: 210 21.4% 94.5%;
--card-raised: 240 11.7% 15.1%;
--primary: 198.7 73.2% 49.8%;
--primary-foreground: 253.3 20.9% 8.4%;
--secondary: 334.1 74.7% 52%;
--secondary-foreground: 0 0% 100%;
--accent: 244.6 11.5% 22.2%;
--accent-foreground: 210 21.4% 94.5%;
--constructive: 128.2 44.2% 44.3%;
--constructive-foreground: 253.3 20.9% 8.4%;
--destructive: 6.2 81.7% 62.7%;
--destructive-foreground: 253.3 20.9% 8.4%;
--ring: 198.7 73.2% 49.8%;
--highlight: 198.7 73.2% 49.8%;
--sidebar-background: 244.6 11.5% 22.2%;
--sidebar-foreground: 210 21.4% 94.5%;
--sidebar-primary: 198.7 73.2% 49.8%;
--sidebar-primary-foreground: 253.3 20.9% 8.4%;
--sidebar-accent: 240 11.7% 15.1%;
--sidebar-accent-foreground: 210 21.4% 94.5%;
--sidebar-border: 240 11.7% 15.1%;
--sidebar-ring: 198.7 73.2% 49.8%;
--app-header-background: hsl(var(--sidebar-background));
--app-header-foreground: hsl(var(--sidebar-foreground));
--app-header-border: hsl(var(--sidebar-border));
--column: 240 8.1% 21.8%; /* #33333C */
--column-foreground: 0 0% 93.3%;
--column-border: 240 4% 34.7%;
--board-background: 240 11.7% 15.1%; /* #22222B */
--board-action-foreground: #EEF1F4;
--canvas: 253.3 20.9% 8.4%;
--np-dialog-surface: #22222B;
--np-dialog-line: rgba(192, 193, 210, 0.18);
--np-scrim: rgba(19, 17, 26, 0.68);
--np-dialog-shadow: 0 24px 64px rgba(0, 0, 0, 0.56), inset 0 0 0 1px rgba(238, 241, 244, 0.025);
--np-popover-shadow: 0 10px 26px rgba(0, 0, 0, 0.44);
}
:root[data-brand="novelpad"][data-novel-theme="novelpad-classic"],
.dark[data-brand="novelpad"][data-novel-theme="novelpad-classic"] {
/* Outfit is already self-hosted and is the closest available geometric sans
to Classic's Montserrat without introducing a network-loaded font. */
--font-sans: "Outfit Variable", "Outfit", "Montserrat", "Roboto", "Helvetica", "Arial", sans-serif;
--font-serif: "Outfit Variable", "Outfit", "Montserrat", "Roboto", "Helvetica", "Arial", sans-serif;
--font-reading: "Libre Baskerville", Georgia, serif;
--font-display: "Outfit Variable", "Outfit", "Montserrat", "Roboto", "Helvetica", "Arial", sans-serif;
--np-gradient: linear-gradient(-45deg, #E02978 45%, #22A2DC 85%);
--np-gradient-soft: linear-gradient(-45deg, rgba(224, 41, 120, 0.18) 45%, rgba(34, 162, 220, 0.18) 85%);
--np-brand-ink: #22A2DC;
--np-brand-soft: rgba(34, 162, 220, 0.14);
--np-focus-ring: 0 0 0 2px hsl(var(--background)), 0 0 0 4px #22A2DC;
--tag-cyan: #08B2E3;
--tag-pink: #EE6352;
--tag-plum: #B38CB4;
--tag-amber: #E8AE68;
--tag-sage: #3FA34D;
--radius: 0.25rem;
--radius-sm: 0.25rem;
--radius-xs: 0.25rem;
--radius-base: 0.25rem;
--radius-md: 0.25rem;
--radius-lg: 0.25rem;
--radius-xl: 0.25rem;
--radius-2xl: 0.25rem;
--radius-compact: 0.25rem;
--radius-icon: 0.25rem;
--radius-control: 0.25rem;
--radius-tile: 0.25rem;
--radius-badge: 0.25rem;
--radius-panel: 0.25rem;
--radius-dialog: 0.25rem;
/* Classic used one seven-pixel grid unit between stacks and around the
board edges (../novelpad/src/novelpad/ux/theme.ts). */
--board-column-gap: 0.4375rem;
--board-padding-inline: 0.4375rem;
--board-padding-block-start: 0.4375rem;
--board-padding-block-end: 0.4375rem;
--board-padding-block-end-stacked: 0.4375rem;
--board-header-background: hsl(var(--board-background));
--board-header-foreground: #EEF1F4;
--board-header-muted-background: rgba(238, 241, 244, 0.08);
--board-header-muted-foreground: #C0C1D2;
--board-header-border: transparent;
--board-header-border-width: 0px;
--board-header-control-border: #444253;
--board-footer-background: hsl(var(--board-background));
--board-footer-foreground: #EEF1F4;
--board-footer-muted-foreground: #C0C1D2;
--board-footer-hover-background: rgba(238, 241, 244, 0.08);
--board-footer-border: transparent;
--board-footer-border-width: 0px;
--section-column-background: hsl(var(--column));
--section-column-radius: 0.25rem;
}
.btn-gradient {
background: var(--np-gradient);
color: #fff;
}
[data-slot="board-header"] [data-slot="board-type-chip"] {
background-color: var(--board-header-muted-background);
border-color: var(--board-header-control-border);
color: var(--board-header-muted-foreground);
}
@layer base {
* {
@apply border-border;
}
body {
@apply font-sans antialiased bg-background text-foreground;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/*
* --mention-ink: the link color for @-mention chips (and the active typeahead
* highlight). On dark surfaces the bright brand --highlight reads well, but on
* light surfaces the high-luminance accents (cyan/blue/orange) wash out — so we
* deepen them per brand for legible contrast. Resolved against the originating
* element, so it also feeds `::highlight(mention-active)` (mark.css). Kept out
* of @layer base so these win over the layered theme blocks.
*/
:root {
--mention-ink: var(--highlight);
}
:root:not(.dark) {
--mention-ink: 217 78% 48%;
}
:root:not(.dark)[data-brand="helmdocs"] {
--mention-ink: 24.6 88% 44%;
}
:root:not(.dark)[data-brand="novelpad"] {
--mention-ink: 192 80% 30%;
}
:root:not(.dark)[data-brand="novelpad"][data-novel-theme="novelpad-classic"] {
--mention-ink: 198.7 83% 34%;
}
/*
* Loader + skeleton design tokens (see docs/features/loaders.md). These feed the
* brand loader system in packages/app/src/components/common/loaders/:
* --loader-gradient the brand sweep used by Spinner / Progress / skeleton stripe
* --np-skel-track resting "bone" fill of a skeleton block
* --np-skel-tint travelling highlight on the skeleton sweep
* --np-skel-line skeleton card hairline (defaults to the theme border)
* --np-skel-speed skeleton shimmer period (overridable inline per instance)
* The handoffs are dark-tuned, so the light values raise contrast. Kept out of
* @layer base so they win over the layered theme/brand blocks. NovelPad is the
* default brand; helmdocs overrides keep the system on-brand if a *shared*
* loader renders on a Helm surface (Helm skeletons themselves stay animate-pulse).
*/
:root {
--loader-gradient: linear-gradient(135deg, #22d3ee 0%, #d946ef 55%, #ec4899 100%);
--np-skel-track: rgba(20, 18, 15, 0.06);
--np-skel-tint: rgba(34, 211, 238, 0.18);
--np-skel-line: hsl(var(--border));
--np-skel-speed: 1400ms;
}
.dark {
--np-skel-track: rgba(255, 255, 255, 0.07);
--np-skel-tint: rgba(150, 228, 247, 0.17);
}
/*
* Full-screen loading surface + inks (LoadingScreen / desktop boot splash).
* Light = NovelPad paper (#FBF8F3 = the light --background) under a faint brand
* glow; dark = the deep ink navy from the loader handoff. The desktop splash
* (apps/desktop/src/renderer/src/splash.tsx) renders before this sheet is
* guaranteed, so it mirrors these values inline — keep the two in sync.
*/
:root {
--np-loading-surface: radial-gradient(120% 90% at 50% 42%, rgba(217, 70, 239, 0.10) 0%, rgba(34, 211, 238, 0.07) 32%, rgba(251, 248, 243, 0) 64%), #FBF8F3;
--np-loading-ink: #1C1A15;
--np-loading-muted: rgba(28, 26, 21, 0.60);
--np-loading-subtitle: rgba(28, 26, 21, 0.45);
--np-loading-line: rgba(28, 26, 21, 0.16);
}
.dark {
--np-loading-surface: radial-gradient(120% 90% at 50% 42%, rgba(217, 70, 239, 0.10) 0%, rgba(34, 211, 238, 0.06) 32%, rgba(15, 21, 39, 0) 64%), #0B0F1A;
--np-loading-ink: #F5F7FA;
--np-loading-muted: rgba(245, 247, 250, 0.55);
--np-loading-subtitle: rgba(245, 247, 250, 0.40);
--np-loading-line: rgba(245, 247, 250, 0.16);
}
/* HelmDocs: plain theme surfaces, no NovelPad glow. */
:root[data-brand="helmdocs"] {
--np-loading-surface: hsl(0 0% 100%);
--np-loading-ink: hsl(20 14.3% 4.1%);
}
.dark[data-brand="helmdocs"] {
--np-loading-surface: hsl(20 14.3% 4.1%);
--np-loading-ink: hsl(60 9.1% 97.8%);
}
:root[data-brand="helmdocs"] {
--loader-gradient: linear-gradient(135deg, hsl(24.6 95% 53.1%) 0%, hsl(20.5 90.2% 48.2%) 100%);
--np-skel-tint: rgba(251, 146, 60, 0.16);
/* Spinner (loader #4) conic sweep — retinted to the HelmDocs orange ramp so
the shared Spinner / loaderToast reads on-brand instead of leaking the
NovelPad cyan→plum→pink sweep. The stops map to spinner.css's conic slots
(transparent → light 180deg → mid 280deg → deep 360deg). */
--np-spinner-from: rgba(251, 146, 60, 0);
--np-spinner-cyan: #fb923c;
--np-spinner-plum: #f97316;
--np-spinner-pink: #ea580c;
}
.dark[data-brand="helmdocs"] {
--np-skel-tint: rgba(251, 146, 60, 0.14);
--np-spinner-from: rgba(251, 146, 60, 0);
--np-spinner-cyan: #fb923c;
--np-spinner-plum: #f97316;
--np-spinner-pink: #ea580c;
}
:root[data-brand="novelpad"][data-novel-theme="novelpad-classic"] {
--loader-gradient: linear-gradient(-45deg, #E02978 45%, #22A2DC 85%);
--np-skel-track: rgba(68, 66, 83, 0.09);
--np-skel-tint: rgba(34, 162, 220, 0.18);
--np-loading-surface: #F3F3F0;
--np-loading-ink: #444253;
--np-loading-muted: rgba(68, 66, 83, 0.66);
--np-loading-subtitle: rgba(68, 66, 83, 0.48);
--np-loading-line: rgba(68, 66, 83, 0.20);
}
.dark[data-brand="novelpad"][data-novel-theme="novelpad-classic"] {
--np-skel-track: rgba(238, 241, 244, 0.08);
--np-skel-tint: rgba(34, 162, 220, 0.18);
--np-loading-surface: #13111A;
--np-loading-ink: #EEF1F4;
--np-loading-muted: rgba(238, 241, 244, 0.60);
--np-loading-subtitle: rgba(238, 241, 244, 0.44);
--np-loading-line: rgba(238, 241, 244, 0.18);
}

View File

@@ -0,0 +1,125 @@
import { type Config } from 'tailwindcss';
export default {
darkMode: 'class',
content: [
'../core/src/**/*.{js,ts,jsx,tsx}',
'../app/src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)'],
serif: ['var(--font-serif)'],
reading: ['var(--font-reading)'],
mono: ['var(--font-mono)'],
display: ['var(--font-display)'],
},
zIndex: {
base: '100',
modal: '200',
overlay: '300',
popover: '400',
tooltip: '500',
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
// Brand accent for highlighting/active states (NovelPad cyan, HelmDocs
// orange). The alpha-value placeholder lets utilities tint it, e.g.
// `text-highlight`, `bg-highlight/15`, `border-highlight/40`.
highlight: 'hsl(var(--highlight) / <alpha-value>)',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
brand: {
DEFAULT: '#22D3EE',
blue: '#22D3EE',
purple: '#D946EF',
pink: '#EC4899',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
constructive: {
DEFAULT: 'hsl(var(--constructive))',
foreground: 'hsl(var(--constructive-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
column: {
DEFAULT: 'hsl(var(--column))',
foreground: 'hsl(var(--column-foreground))',
border: 'hsl(var(--column-border))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))',
},
},
borderRadius: {
sm: 'var(--radius-sm)',
DEFAULT: 'var(--radius-base)',
md: 'var(--radius-md)',
lg: 'var(--radius-lg)',
xl: 'var(--radius-xl)',
'2xl': 'var(--radius-2xl)',
},
aspectRatio: {
'3/2': '3 / 2',
},
keyframes: {
'accordion-down': {
from: {
height: '0',
},
to: {
height: 'var(--radix-accordion-content-height)',
},
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)',
},
to: {
height: '0',
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography'), require('@tailwindcss/container-queries')],
} satisfies Config;

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"module": "NodeNext",
"jsx": "react-jsx",
"sourceMap": true,
"incremental": true
}
}

View File

@@ -0,0 +1,32 @@
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import url from 'node:url';
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
export function reactVirtualized() {
return {
name: 'flat:react-virtualized',
// Note: we cannot use the `transform` hook here
// because libraries are pre-bundled in vite directly,
// plugins aren't able to hack that step currently.
// so instead we manually edit the file in node_modules.
// all we need is to find the timing before pre-bundling.
configResolved: async () => {
const require = createRequire(import.meta.url);
const reactVirtualizedPath = require.resolve('react-virtualized');
const { pathname: reactVirtualizedFilePath } = new url.URL(
reactVirtualizedPath,
import.meta.url,
);
const file = reactVirtualizedFilePath.replace(
path.join('dist', 'commonjs', 'index.js'),
path.join('dist', 'es', 'WindowScroller', 'utils', 'onScroll.js'),
);
const code = await fs.readFile(file, 'utf-8');
const modified = code.replace(WRONG_CODE, '');
await fs.writeFile(file, modified);
},
};
}