Add the web app, seed data, and user-selectable theming
apps/web — React, Vite, Tailwind, shadcn-idiom components. Mobile Safari is a first-class target, not an afterthought: - Two navigation treatments rather than one compromise. A bottom tab bar on phones, because the top of a large phone is out of thumb reach; a persistent sidebar from lg upward, so an iPad in portrait gets it too. - Safe-area insets throughout, so the tab bar clears the home indicator and the last row of a list is actually reachable. - Inputs are pinned to a 16px minimum, which is the correct fix for Safari zooming on focus. user-scalable=no is not used: it breaks pinch-zoom for everyone and recent iOS ignores it anyway. - The pipeline board becomes a stage picker on phones. An eight-column board scrolling horizontally on a 390px screen is technically responsive and practically useless. Theming: users pick an accent and the whole interface re-tints. Accent values live once, in @pig/core, and are written onto the root element at runtime — there is no CSS copy to drift from the TypeScript. Preferences are stored server-side so they follow a person between laptop and phone, mirrored into localStorage only so the pre-paint script can avoid a white flash. Status colours stay fixed regardless of accent: if "at risk" re-tinted to whatever someone picked, the signal would be gone. Seed data is public research, every record carrying a confidence grade and a source URL. No email addresses are seeded or inferred — none are published, and guessing them from a name and a domain is unreliable and rude. Authorship is not promoted to employment: contributors, residency participants and alumni are recorded as what the evidence actually shows, and a name that could not be sourced at all is listed as unresolved rather than invented. Three defects found and fixed by actually running it rather than assuming: 1. The seed was not idempotent. onConflictDoNothing() with no target is a no-op without a matching unique constraint, so a second run duplicated 27 contacts. There is deliberately no unique index on (account, name) — two people at one company can share a name — so idempotency is enforced in the seed instead of by bending the schema. 2. /capacity scrolled sideways on a phone. Grid items default to min-width:auto and `truncate` sets nowrap, so a long title became unshrinkable content and widened the track. Fixed with min-w-0 on every truncating grid child. 3. The idle-capacity alert silently failed to fire at exactly 80% utilisation, losing a float comparison against a 0.2 threshold. Moved to 0.15, which is also a more sensible line for "worth attention". The worked example is tuned to teach rather than to flatter: 70% sold at a 53% markup lands at +6.7% margin with 20% still idle, so both the healthy number and the alert are visible. Drop the sold share to 55% and the same block goes underwater — that sensitivity is the argument for the product. Verified in a real browser at 393px and 1440px, light and dark: zero horizontal overflow on every route, zero console errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/*
|
||||
* Surfaces and neutrals.
|
||||
*
|
||||
* Accent variables are NOT defined here — they are written onto the root
|
||||
* element at runtime from the palette in @pig/core, so there is exactly one
|
||||
* definition of each colour and the CSS cannot drift from the TypeScript.
|
||||
*/
|
||||
:root {
|
||||
--bg: 0 0% 100%;
|
||||
--surface: 0 0% 100%;
|
||||
--surface-2: 240 5% 97%;
|
||||
--border: 240 6% 90%;
|
||||
--fg: 240 10% 4%;
|
||||
--muted: 240 4% 46%;
|
||||
|
||||
--positive: 160 84% 32%;
|
||||
--warning: 32 95% 44%;
|
||||
--danger: 0 72% 45%;
|
||||
--info: 201 90% 40%;
|
||||
|
||||
/* Safe-area insets, so layout can reference them even at zero. */
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-left: env(safe-area-inset-left, 0px);
|
||||
--safe-right: env(safe-area-inset-right, 0px);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--bg: 240 10% 4%;
|
||||
--surface: 240 8% 7%;
|
||||
--surface-2: 240 6% 11%;
|
||||
--border: 240 5% 18%;
|
||||
--fg: 0 0% 98%;
|
||||
--muted: 240 5% 60%;
|
||||
|
||||
--positive: 158 64% 52%;
|
||||
--warning: 38 92% 60%;
|
||||
--danger: 0 84% 65%;
|
||||
--info: 199 89% 60%;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
html {
|
||||
/* Prevents iOS from silently enlarging text in landscape. */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* Keeps the background painted behind the rubber-band overscroll area. */
|
||||
background-color: hsl(var(--bg));
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-bg text-fg antialiased;
|
||||
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||
/* Stops the whole page bouncing on iOS while inner panes still scroll. */
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mobile Safari zooms the viewport when a focused input has a font size
|
||||
* below 16px, and never zooms back out. This is the fix — not
|
||||
* user-scalable=no, which breaks pinch-zoom for everyone.
|
||||
*/
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: max(16px, 1rem);
|
||||
}
|
||||
|
||||
/* A visible, consistent focus ring — keyboard users need it, and the
|
||||
default varies wildly between browsers. */
|
||||
:focus-visible {
|
||||
@apply outline-none ring-2 ring-accent ring-offset-2 ring-offset-bg;
|
||||
}
|
||||
|
||||
/* Respect a reduced-motion preference rather than animating regardless. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Horizontal scrollers (pipeline boards, wide tables) get momentum
|
||||
scrolling and snap on touch, without hijacking the page. */
|
||||
.scroll-x {
|
||||
@apply overflow-x-auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.scroll-x::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
.scroll-x::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Touch targets. 44px is the documented iOS minimum and the difference
|
||||
between a usable and a frustrating phone experience. */
|
||||
.tap {
|
||||
@apply min-h-[44px] min-w-[44px];
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-xl border border-border bg-surface;
|
||||
}
|
||||
|
||||
/* Tabular figures keep money and percentages from jittering as they
|
||||
update, which matters on a dashboard that refreshes. */
|
||||
.nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user