Update CBS Mobile App codebase
This commit is contained in:
+641
-94
@@ -1,42 +1,558 @@
|
||||
import type { ViewStyle } from 'react-native';
|
||||
|
||||
import { dark, light, type CategoryHue, type Palette } from './palette';
|
||||
|
||||
/**
|
||||
* BankCBS design tokens.
|
||||
*
|
||||
* ── WHERE THINGS LIVE ───────────────────────────────────────────────────────
|
||||
* palette.ts the two colour ramps. THAT is the file you edit to change a
|
||||
* colour; it is the only place a hex value is written.
|
||||
* tokens.ts this file. Maps a ramp onto the semantic roles screens
|
||||
* actually use, and holds everything that does NOT change
|
||||
* between light and dark: the type scale, radius, spacing and
|
||||
* layout constants.
|
||||
*
|
||||
* The mapping is written once, in `buildTheme`, and applied to both ramps. So
|
||||
* a role's meaning cannot drift between the two themes — if `primarySurface`
|
||||
* means "the tint behind a selected row", it means that in both, and adding a
|
||||
* role to one theme without the other is a compile error.
|
||||
*
|
||||
* ── HOW A SCREEN USES THIS ──────────────────────────────────────────────────
|
||||
* const theme = useTheme();
|
||||
* <Text style={[theme.text.h2, { color: theme.colors.textPrimary }]}>
|
||||
*
|
||||
* Colours must be read through `useTheme()`, because they change at runtime
|
||||
* when the theme does. The TYPE SCALE does not change between themes, so it
|
||||
* can also be imported directly for use inside `StyleSheet.create`, where
|
||||
* there is no hook:
|
||||
*
|
||||
* import { textStyles } from '../../theme';
|
||||
* const styles = StyleSheet.create({ title: { ...textStyles.h3 } });
|
||||
*
|
||||
* That is the supported way to set type in a static stylesheet. Writing a raw
|
||||
* `fontSize` / `fontWeight` pair instead is what this file exists to replace —
|
||||
* see the typography note below for why `fontWeight` in particular is a bug
|
||||
* on Android rather than a style choice.
|
||||
*
|
||||
* React Native constraints this file respects: one outer shadow per element,
|
||||
* with Android ignoring its colour and offset entirely — so depth is carried
|
||||
* by surface-tone steps and real borders, and the shadow is only a lift on
|
||||
* iOS. No blur, no inner or stacked shadows, no coloured glows.
|
||||
*/
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* TYPOGRAPHY — identical in both themes.
|
||||
*
|
||||
* Two families, each with a job:
|
||||
*
|
||||
* IBM PLEX SANS — everything that is language. A grotesque drawn for
|
||||
* technical and institutional work, which is the register a
|
||||
* bank wants. Static weights: 400, 500, 600, 700.
|
||||
* IBM PLEX MONO — everything that is a figure or a fixed-format code:
|
||||
* balances, amounts in a column, account numbers, IFSC
|
||||
* codes, reference numbers. Weights: 400, 600.
|
||||
*
|
||||
* The split is functional, not decorative. Money has to align on the decimal
|
||||
* down a column, and a proportional face cannot do that without an OpenType
|
||||
* feature Android applies inconsistently. Monospace gets it for nothing.
|
||||
*
|
||||
* ── WHAT WAS HERE BEFORE, AND WHY IT IS GONE ────────────────────────────────
|
||||
* This file used to declare Nunito. The four Nunito .ttf files in the repo
|
||||
* were not fonts: `download-fonts.js` had been run after its source URLs died
|
||||
* and, with no status check, wrote GitHub's 404 page into each one — four
|
||||
* 300KB HTML documents with a .ttf extension. React Native does not validate
|
||||
* a font asset, so Android fell back to Roboto: the app shipped in the system
|
||||
* font while every style in here named a typeface it had never loaded. The
|
||||
* download script now verifies the TrueType magic number before keeping a file.
|
||||
*
|
||||
* ── SELECTING A WEIGHT ──────────────────────────────────────────────────────
|
||||
* Weight is selected by FONT FAMILY, never by `fontWeight`. Setting both is
|
||||
* the documented way to get the wrong face on Android, and setting only
|
||||
* `fontWeight` gets you the SYSTEM font at that weight — not Plex at all —
|
||||
* which is why every style below carries `fontFamily` and deliberately omits
|
||||
* `fontWeight`.
|
||||
*
|
||||
* The filenames are the fonts' own PostScript names — `IBMPlexSans-SmBld`,
|
||||
* not `IBMPlexSans-SemiBold` — because iOS resolves a family against the
|
||||
* PostScript name and Android against the asset filename, and the two only
|
||||
* agree if the file is named after the former. Do not tidy those names.
|
||||
*
|
||||
* Plex is already an institutional face, so the formality no longer has to be
|
||||
* manufactured by the type scale. What the scale still does deliberately:
|
||||
* - mono for every figure, so columns of money line up;
|
||||
* - Medium reserved for emphasis and SemiBold for controls, so "important"
|
||||
* and "tappable" stop looking like the same thing;
|
||||
* - uppercase with wide tracking on labels and eyebrows.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
export const fontFamily = {
|
||||
/* ── IBM Plex Sans: everything that is language ─────────────────────────── */
|
||||
regular: 'IBMPlexSans', // 400
|
||||
medium: 'IBMPlexSans-Medm', // 500
|
||||
semibold: 'IBMPlexSans-SmBld', // 600
|
||||
bold: 'IBMPlexSans-Bold', // 700 — the heaviest weight Plex ships
|
||||
|
||||
/* ── IBM Plex Mono: everything that is a figure or a code ───────────────── */
|
||||
mono: 'IBMPlexMono', // 400
|
||||
monoSemibold: 'IBMPlexMono-SmBld', // 600
|
||||
|
||||
/**
|
||||
* @deprecated Plex has no ExtraBold — Bold is its heaviest weight. Mapped
|
||||
* here so existing call sites keep rendering a real face instead of falling
|
||||
* back to the system font. Use `bold`.
|
||||
*/
|
||||
extrabold: 'IBMPlexSans-Bold',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Ready-to-spread text styles. Spread one, then set only `color`:
|
||||
*
|
||||
* <Text style={[theme.text.h2, { color: theme.colors.textPrimary }]}>
|
||||
*
|
||||
* or, inside a static stylesheet where there is no hook:
|
||||
*
|
||||
* const styles = StyleSheet.create({ title: { ...textStyles.h2 } });
|
||||
*
|
||||
* Note on money alignment: `balance`, `money`, `moneyLarge` and `code` are set
|
||||
* in Plex Mono, so their digits are already one width and no `fontVariant`
|
||||
* feature flag is involved. Right-align a column of amounts all the same — the
|
||||
* font holds the digits on a grid, and the alignment is what puts the decimal
|
||||
* points above one another.
|
||||
*/
|
||||
const text = {
|
||||
/**
|
||||
* The hero balance figure. One per screen, at most.
|
||||
*
|
||||
* Set in MONO. A balance is a figure, not a sentence: monospace digits are
|
||||
* all one width, so the number does not reflow as it changes and the decimal
|
||||
* holds its position. Slightly smaller than the sans equivalent would be,
|
||||
* because mono runs wider per character.
|
||||
*/
|
||||
balance: {
|
||||
fontFamily: fontFamily.monoSemibold,
|
||||
fontSize: 36,
|
||||
lineHeight: 44,
|
||||
letterSpacing: -1,
|
||||
},
|
||||
/**
|
||||
* The largest text on a screen — an onboarding headline, a screen title that
|
||||
* has to carry the whole page. Sans, not mono: this is language. A figure
|
||||
* that needs this much presence uses `balance`.
|
||||
*/
|
||||
display: {
|
||||
fontFamily: fontFamily.bold,
|
||||
fontSize: 32,
|
||||
lineHeight: 39,
|
||||
letterSpacing: -0.7,
|
||||
},
|
||||
h1: {
|
||||
fontFamily: fontFamily.bold,
|
||||
fontSize: 26,
|
||||
lineHeight: 32,
|
||||
letterSpacing: -0.4,
|
||||
},
|
||||
h2: {
|
||||
fontFamily: fontFamily.bold,
|
||||
fontSize: 21,
|
||||
lineHeight: 27,
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: fontFamily.bold,
|
||||
fontSize: 17,
|
||||
lineHeight: 23,
|
||||
letterSpacing: -0.1,
|
||||
},
|
||||
/**
|
||||
* Body, emphasised — a row title, a name.
|
||||
*
|
||||
* Medium, a weight Nunito never had. It leaves SemiBold free to mean
|
||||
* "this is a control" on buttons and labels, so emphasis and interactivity
|
||||
* stop looking like the same thing.
|
||||
*/
|
||||
bodyStrong: {
|
||||
fontFamily: fontFamily.medium,
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
body: {
|
||||
fontFamily: fontFamily.regular,
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
bodySmall: {
|
||||
fontFamily: fontFamily.regular,
|
||||
fontSize: 13,
|
||||
lineHeight: 19,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
caption: {
|
||||
fontFamily: fontFamily.regular,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0.1,
|
||||
},
|
||||
/**
|
||||
* Uppercase eyebrow / field label. The wide tracking is where the
|
||||
* institutional formality comes from — do not reduce it. SemiBold rather
|
||||
* than Bold: Plex Bold set in caps at 11pt reads as a shout.
|
||||
*/
|
||||
eyebrow: {
|
||||
fontFamily: fontFamily.semibold,
|
||||
fontSize: 11,
|
||||
lineHeight: 14,
|
||||
letterSpacing: 0.9,
|
||||
textTransform: 'uppercase' as const,
|
||||
},
|
||||
button: {
|
||||
fontFamily: fontFamily.semibold,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
/**
|
||||
* Money in a list or table. Right-align it.
|
||||
*
|
||||
* Mono, which is what finally makes a column of amounts line up. The old
|
||||
* sans version asked for the same thing with `fontVariant:
|
||||
* ['tabular-nums']` — an OpenType feature iOS honours and Android applies
|
||||
* inconsistently, so the decimals drifted on most of the user base.
|
||||
* Monospace needs no feature flag: every digit is the same width by
|
||||
* construction.
|
||||
*/
|
||||
money: {
|
||||
fontFamily: fontFamily.monoSemibold,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
/** Money that carries the screen — a detail header, a confirmation. */
|
||||
moneyLarge: {
|
||||
fontFamily: fontFamily.monoSemibold,
|
||||
fontSize: 19,
|
||||
lineHeight: 25,
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
/**
|
||||
* Fixed-format identifiers: IFSC, account number, reference number, a masked
|
||||
* mobile. Mono, so the characters sit on a grid and a customer can read one
|
||||
* back over the phone without losing their place.
|
||||
*
|
||||
* Tracking is 0.4 rather than the 1.2 this carried as a proportional face —
|
||||
* that width was propping up a font that did not have it, and mono already
|
||||
* does.
|
||||
*/
|
||||
code: {
|
||||
fontFamily: fontFamily.mono,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
letterSpacing: 0.4,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The type scale, importable without a hook.
|
||||
*
|
||||
* Type does not change between light and dark — only colour does — so a static
|
||||
* `StyleSheet.create` can spread these directly and still be part of the
|
||||
* design system. Set the colour separately, from `useTheme()`.
|
||||
*/
|
||||
export const textStyles = text;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* RADIUS
|
||||
*
|
||||
* Radius is assigned by role, not by size. One uniform corner stamped on every
|
||||
* element flattens hierarchy — a full-bleed hero and a 20pt status pill should
|
||||
* not share a corner.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const radius = {
|
||||
sm: 6, // chips, badges, small inline marks
|
||||
md: 10, // buttons, inputs, list rows
|
||||
lg: 14, // cards
|
||||
xl: 18, // the hero balance card, promo surfaces
|
||||
xxl: 24, // bottom sheet top corners
|
||||
pill: 999, // segmented control thumb, filter chips, status pills
|
||||
} as const;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* SPACING — a 4pt grid.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const spacing = {
|
||||
xxs: 4,
|
||||
xs: 8,
|
||||
sm: 12,
|
||||
md: 16, // the standard screen gutter
|
||||
lg: 20,
|
||||
xl: 24,
|
||||
xxl: 32,
|
||||
xxxl: 48,
|
||||
} as const;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* LAYOUT — platform facts screens have to design around.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const layout = {
|
||||
/** The bottom tab bar floats above content at this height. */
|
||||
tabBarHeight: 90,
|
||||
/** Bottom padding a scrolling page must reserve so its last row is not
|
||||
* trapped behind the floating tab bar. */
|
||||
scrollBottomInset: 106,
|
||||
/** Minimum touch target. Agent screens use the larger one — that person is
|
||||
* working one-handed, at speed, standing up. */
|
||||
touchTarget: 44,
|
||||
touchTargetAgent: 48,
|
||||
/** Standard control heights. */
|
||||
controlHeight: 52,
|
||||
controlHeightAgent: 56,
|
||||
controlHeightSmall: 40,
|
||||
/** The screen gutter. */
|
||||
gutter: 16,
|
||||
hairline: 1,
|
||||
} as const;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* ELEVATION
|
||||
*
|
||||
* React Native allows exactly one outer shadow per element, and Android
|
||||
* ignores its colour and offset — it only reads `elevation`. A design that
|
||||
* leans on a soft coloured shadow therefore looks flat for most of the user
|
||||
* base, so each level pairs its shadow with a `borderColor`, and the border is
|
||||
* what actually separates the surface on Android.
|
||||
*
|
||||
* Built per theme, because both halves of that are theme-dependent: the border
|
||||
* comes from the ramp, and a shadow does almost nothing on a dark ground. In
|
||||
* dark mode the shadow is kept (it still reads faintly on iOS against the
|
||||
* lighter card tone) but separation is really carried by the surface steps and
|
||||
* the border — which is why the dark ramp measures each step against its
|
||||
* neighbour.
|
||||
*
|
||||
* Spread these onto a view that already has a backgroundColor and borderWidth.
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const buildElevation = (p: Palette) =>
|
||||
({
|
||||
/** Flat. A border does all the work — grouped rows, inline banners. */
|
||||
level0: {
|
||||
borderColor: p.border,
|
||||
shadowColor: p.shadow,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
/** Resting card. The default for content sitting on the app background. */
|
||||
level1: {
|
||||
borderColor: p.border,
|
||||
shadowColor: p.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 3,
|
||||
elevation: 1,
|
||||
},
|
||||
/** Raised — a pinned footer, a sticky header, the hero balance card. */
|
||||
level2: {
|
||||
borderColor: p.border,
|
||||
shadowColor: p.shadow,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 10,
|
||||
elevation: 4,
|
||||
},
|
||||
/** Overlay — bottom sheets and modals, which sit above a scrim. */
|
||||
level3: {
|
||||
borderColor: p.borderStrong,
|
||||
shadowColor: p.shadow,
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.16,
|
||||
shadowRadius: 22,
|
||||
elevation: 12,
|
||||
},
|
||||
}) as const;
|
||||
|
||||
type Elevation = ReturnType<typeof buildElevation>;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
* THEME
|
||||
* ──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** Which ramp a theme was built from. */
|
||||
export type ThemeName = 'light' | 'dark';
|
||||
|
||||
export interface FintechTheme {
|
||||
/** Which ramp this is. For the rare case that genuinely has to branch —
|
||||
* picking one of two image assets, say. Never for colour: that is what the
|
||||
* roles below are for. */
|
||||
name: ThemeName;
|
||||
/** Convenience for the same thing. */
|
||||
isDark: boolean;
|
||||
|
||||
/**
|
||||
* Status-bar icon style for a screen sitting on `colors.background`.
|
||||
*
|
||||
* Derived here so the light/dark ternary is written once. A screen on the
|
||||
* full-bleed `brandGradient` does NOT use this — that wash is dark in both
|
||||
* themes, so those screens hardcode `light-content` and are correct to.
|
||||
*/
|
||||
statusBarStyle: 'light-content' | 'dark-content';
|
||||
|
||||
colors: {
|
||||
/* ── surfaces ─────────────────────────────────────────────────────────── */
|
||||
/** App background. */
|
||||
background: string;
|
||||
backgroundElevated: string;
|
||||
/** Vertical wash for a full-bleed header. */
|
||||
backgroundGradient: [string, string, string];
|
||||
/** Cards. Primary text on this is 14.63:1 or better in both themes. */
|
||||
surface: string;
|
||||
/** Muted surface — pressed rows, wells, grouped backgrounds. */
|
||||
surfaceMuted: string;
|
||||
/**
|
||||
* Sunk surface. NON-TEXT: progress tracks, skeleton bases, keypad wells,
|
||||
* disabled fills. Only `textPrimary`, `textSecondary` and `primary` are
|
||||
* legible on it — muted text, success and the accent all fall below AA
|
||||
* here, so do not put them on this surface.
|
||||
*/
|
||||
surfaceSunk: string;
|
||||
/** @deprecated There is no blur library in this app, so nothing is glass.
|
||||
* Kept only so existing callers still compile; it resolves to `surface`.
|
||||
* Use `surface`. */
|
||||
cardGlass: string;
|
||||
/** Default border. Visible against a card in both themes — the original
|
||||
* rgba(0,0,0,0.05) was not, which is why cards had no edges. */
|
||||
cardBorder: string;
|
||||
/** Stronger border — a focused field, a selected card, a divider that has
|
||||
* to carry weight on its own. */
|
||||
cardBorderStrong: string;
|
||||
|
||||
/* ── text ─────────────────────────────────────────────────────────────── */
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
/** Placeholders, timestamps, helper text. */
|
||||
textMuted: string;
|
||||
/**
|
||||
* For text and icons sitting on a saturated fill — a navy, danger or
|
||||
* success button.
|
||||
*
|
||||
* NOT always white. It is white in light mode and dark ink in dark mode,
|
||||
* because dark mode lightens its tones and white on a lightened tone is
|
||||
* about 2.6:1. This is the role that makes a hardcoded '#FFF' on a button
|
||||
* a real bug rather than a shortcut.
|
||||
*/
|
||||
textOnBrand: string;
|
||||
|
||||
/* ── brand ────────────────────────────────────────────────────────────── */
|
||||
/** Primary actions, active states, brand moments. #124577 in light mode;
|
||||
* a lighter navy in dark, where #124577 would be invisible. */
|
||||
primary: string;
|
||||
/** Lighter brand — the second stop of a gradient. Not a sky blue. */
|
||||
primaryLight: string;
|
||||
/** Pressed state for a brand fill. */
|
||||
primaryDark: string;
|
||||
/** Deepest brand tone — a full-bleed header. */
|
||||
primaryDeep: string;
|
||||
/** Brand tint — a selected row, a brand banner, an active chip. */
|
||||
primarySurface: string;
|
||||
primaryBorder: string;
|
||||
/**
|
||||
* The brand wash for a full-bleed screen, lightest stop first. Sign-in and
|
||||
* registration use it; everywhere else is content on the app background.
|
||||
* Requires LIGHT status-bar icons in both themes.
|
||||
*/
|
||||
brandGradient: [string, string, string];
|
||||
|
||||
/* ── on-brand: text and fills for use ON the wash above ───────────────── */
|
||||
/** Solid white. The only tier legal on top of a translucent fill. */
|
||||
onBrandPrimary: string;
|
||||
/** Body-safe on the bare wash. */
|
||||
onBrandSecondary: string;
|
||||
/** The quietest line, still AA on the bare wash. */
|
||||
onBrandMuted: string;
|
||||
/** Low-opacity white fill for a tile. */
|
||||
onBrandSurface: string;
|
||||
/** A heavier fill, for a secondary button. This is the ceiling — see the
|
||||
* note in palette.ts. */
|
||||
onBrandSurfaceStrong: string;
|
||||
onBrandBorder: string;
|
||||
|
||||
/* ── accent (brass) ───────────────────────────────────────────────────── */
|
||||
/** Text-safe brass. Also safe as a fill with `textOnBrand` on it. */
|
||||
accent: string;
|
||||
/** Pressed accent. */
|
||||
accentStrong: string;
|
||||
/** Vivid brass fill. Pair it with `textOnAccentFill` — never
|
||||
* `textOnBrand`, which is 2.26:1 on it in light mode. */
|
||||
accentFill: string;
|
||||
/** The tone that sits on `accentFill`: dark ink in both themes. Use this
|
||||
* rather than `textPrimary`, which is near-white in dark mode and 1.99:1
|
||||
* on the brass. */
|
||||
textOnAccentFill: string;
|
||||
accentSurface: string;
|
||||
accentBorder: string;
|
||||
|
||||
/* ── semantic ─────────────────────────────────────────────────────────── */
|
||||
/** A credit, a completed transfer, an active account. Always paired with
|
||||
* an icon or a sign — never colour alone. */
|
||||
success: string;
|
||||
successSurface: string;
|
||||
successBorder: string;
|
||||
warning: string;
|
||||
warningSurface: string;
|
||||
warningBorder: string;
|
||||
danger: string;
|
||||
dangerSurface: string;
|
||||
dangerBorder: string;
|
||||
info: string;
|
||||
overlay: string;
|
||||
white: string;
|
||||
infoSurface: string;
|
||||
infoBorder: string;
|
||||
/** Alias of `danger`, kept because ~30 call sites use it for field errors. */
|
||||
error: string;
|
||||
// Cyber Nexus Accents
|
||||
|
||||
/* ── categorical ──────────────────────────────────────────────────────── */
|
||||
/**
|
||||
* Decorative accents for telling categories apart — a service tile, a loan
|
||||
* type, a scheme icon. Each hue carries a `tone` for the icon or label and
|
||||
* a `surface` for the tint chip behind it.
|
||||
*
|
||||
* These are NOT states. Do not read success or failure off them; the
|
||||
* semantic roles above are for that.
|
||||
*/
|
||||
category: Record<CategoryHue, { tone: string; surface: string }>;
|
||||
|
||||
/* ── misc ─────────────────────────────────────────────────────────────── */
|
||||
/** Scrim behind a modal or sheet. */
|
||||
overlay: string;
|
||||
/**
|
||||
* iOS-only shadow colour; Android reads `elevation` and discards it.
|
||||
*
|
||||
* Prefer `theme.elevation.level1` and friends, which carry this plus the
|
||||
* border Android actually needs. This is here for a one-off lift that does
|
||||
* not want a whole elevation level.
|
||||
*/
|
||||
shadow: string;
|
||||
/** Literal white, regardless of theme. Almost always the wrong choice —
|
||||
* you probably want `surface` or `textOnBrand`. */
|
||||
white: string;
|
||||
|
||||
/** @deprecated Left over from an earlier "cyber" theme that this app is
|
||||
* not. All three resolve into the brand family so existing call sites
|
||||
* render correctly; use `primary` / `primarySurface` instead. */
|
||||
cyberCyan: string;
|
||||
/** @deprecated Use `primary`. */
|
||||
cyberPurple: string;
|
||||
/** @deprecated Use `primarySurface`. */
|
||||
cyberGlow: string;
|
||||
};
|
||||
spacing: {
|
||||
xxs: number;
|
||||
xs: number;
|
||||
sm: number;
|
||||
md: number;
|
||||
lg: number;
|
||||
xl: number;
|
||||
xxl: number;
|
||||
xxxl: number;
|
||||
};
|
||||
|
||||
spacing: typeof spacing;
|
||||
/** Raw font sizes, kept for existing callers. Prefer `theme.text`, which
|
||||
* carries the family, line height and tracking as well. */
|
||||
typography: {
|
||||
hero: number;
|
||||
h1: number;
|
||||
@@ -46,92 +562,123 @@ export interface FintechTheme {
|
||||
bodySmall: number;
|
||||
caption: number;
|
||||
};
|
||||
radius: {
|
||||
sm: number;
|
||||
md: number;
|
||||
lg: number;
|
||||
xl: number;
|
||||
xxl: number;
|
||||
pill: number;
|
||||
};
|
||||
/** The type scale. Spread a style, then set `color`. */
|
||||
text: typeof text;
|
||||
fontFamily: typeof fontFamily;
|
||||
radius: typeof radius;
|
||||
layout: typeof layout;
|
||||
elevation: Elevation;
|
||||
shadows: {
|
||||
soft: {
|
||||
shadowColor: string;
|
||||
shadowOffset: { width: number; height: number };
|
||||
shadowOpacity: number;
|
||||
shadowRadius: number;
|
||||
elevation: number;
|
||||
};
|
||||
/** @deprecated Use `theme.elevation.level1`, which also carries the border
|
||||
* that Android needs in order to show any separation at all. */
|
||||
soft: ViewStyle;
|
||||
};
|
||||
}
|
||||
|
||||
// Keeping the export name as `darkTheme` for now so we don't break every import,
|
||||
// but the theme values represent the new Light Corporate Design language.
|
||||
export const darkTheme: FintechTheme = {
|
||||
colors: {
|
||||
background: '#F1F5F9', // Light gray background
|
||||
backgroundElevated: '#FFFFFF',
|
||||
backgroundGradient: ['#FFFFFF', '#F8FAFC', '#F1F5F9'],
|
||||
surface: '#FFFFFF',
|
||||
surfaceMuted: '#F8FAFC',
|
||||
cardGlass: 'rgba(255, 255, 255, 0.9)',
|
||||
cardBorder: 'rgba(0, 0, 0, 0.05)',
|
||||
textPrimary: '#0F172A',
|
||||
textSecondary: '#124577',
|
||||
textMuted: '#94A3B8',
|
||||
primary: '#124577', // Brand Light Blue/Cyan
|
||||
primaryLight: '#60A5FA',
|
||||
primaryDark: '#2563EB',
|
||||
accent: '#10B981', // Brand Green
|
||||
accentStrong: '#059669',
|
||||
success: '#10B981',
|
||||
warning: '#F59E0B',
|
||||
danger: '#EF4444',
|
||||
info: '#3B82F6',
|
||||
overlay: 'rgba(0, 0, 0, 0.4)',
|
||||
white: '#FFFFFF',
|
||||
error: '#EF4444',
|
||||
// Cyber Nexus Accents (Resetting to neutral or removing usage)
|
||||
cyberCyan: '#3B82F6',
|
||||
cyberPurple: '#6366F1',
|
||||
cyberGlow: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
spacing: {
|
||||
xxs: 4,
|
||||
xs: 8,
|
||||
sm: 12,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 24,
|
||||
xxl: 32,
|
||||
xxxl: 48,
|
||||
},
|
||||
typography: {
|
||||
hero: 34,
|
||||
h1: 28,
|
||||
h2: 22,
|
||||
h3: 18,
|
||||
body: 16,
|
||||
bodySmall: 14,
|
||||
caption: 12,
|
||||
},
|
||||
radius: {
|
||||
sm: 6,
|
||||
md: 8,
|
||||
lg: 12,
|
||||
xl: 16,
|
||||
xxl: 20,
|
||||
pill: 999,
|
||||
},
|
||||
shadows: {
|
||||
soft: {
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
elevation: 3,
|
||||
/**
|
||||
* Maps one colour ramp onto the semantic roles above.
|
||||
*
|
||||
* Written once and applied to both ramps, so a role cannot come to mean
|
||||
* different things in light and dark. If you are adding a role, add it to
|
||||
* `Palette` in palette.ts first — both ramps then have to supply it, which is
|
||||
* the check that stops a screen from being correct in one theme only.
|
||||
*/
|
||||
const buildTheme = (p: Palette, name: ThemeName): FintechTheme => {
|
||||
const elevation = buildElevation(p);
|
||||
|
||||
return {
|
||||
name,
|
||||
isDark: name === 'dark',
|
||||
statusBarStyle: name === 'dark' ? 'light-content' : 'dark-content',
|
||||
|
||||
colors: {
|
||||
background: p.background,
|
||||
backgroundElevated: p.backgroundElevated,
|
||||
backgroundGradient: p.backgroundGradient,
|
||||
surface: p.surface,
|
||||
surfaceMuted: p.surfaceMuted,
|
||||
surfaceSunk: p.surfaceSunk,
|
||||
cardGlass: p.surface,
|
||||
cardBorder: p.border,
|
||||
cardBorderStrong: p.borderStrong,
|
||||
|
||||
textPrimary: p.textPrimary,
|
||||
textSecondary: p.textSecondary,
|
||||
textMuted: p.textMuted,
|
||||
textOnBrand: p.textOnFill,
|
||||
|
||||
primary: p.brand,
|
||||
primaryLight: p.brandLight,
|
||||
primaryDark: p.brandDark,
|
||||
primaryDeep: p.brandDeep,
|
||||
primarySurface: p.brandSurface,
|
||||
primaryBorder: p.brandBorder,
|
||||
brandGradient: p.brandGradient,
|
||||
|
||||
onBrandPrimary: p.onBrandPrimary,
|
||||
onBrandSecondary: p.onBrandSecondary,
|
||||
onBrandMuted: p.onBrandMuted,
|
||||
onBrandSurface: p.onBrandSurface,
|
||||
onBrandSurfaceStrong: p.onBrandSurfaceStrong,
|
||||
onBrandBorder: p.onBrandBorder,
|
||||
|
||||
accent: p.accent,
|
||||
accentStrong: p.accentStrong,
|
||||
accentFill: p.accentFill,
|
||||
textOnAccentFill: p.textOnAccentFill,
|
||||
accentSurface: p.accentSurface,
|
||||
accentBorder: p.accentBorder,
|
||||
|
||||
success: p.success,
|
||||
successSurface: p.successSurface,
|
||||
successBorder: p.successBorder,
|
||||
warning: p.warning,
|
||||
warningSurface: p.warningSurface,
|
||||
warningBorder: p.warningBorder,
|
||||
danger: p.danger,
|
||||
dangerSurface: p.dangerSurface,
|
||||
dangerBorder: p.dangerBorder,
|
||||
info: p.info,
|
||||
infoSurface: p.infoSurface,
|
||||
infoBorder: p.infoBorder,
|
||||
error: p.danger,
|
||||
category: p.category,
|
||||
|
||||
overlay: p.overlay,
|
||||
shadow: p.shadow,
|
||||
white: p.white,
|
||||
|
||||
cyberCyan: p.brand,
|
||||
cyberPurple: p.brandLight,
|
||||
cyberGlow: p.brandSurface,
|
||||
},
|
||||
},
|
||||
|
||||
spacing,
|
||||
typography: {
|
||||
hero: text.balance.fontSize,
|
||||
h1: text.h1.fontSize,
|
||||
h2: text.h2.fontSize,
|
||||
h3: text.h3.fontSize,
|
||||
body: text.body.fontSize,
|
||||
bodySmall: text.bodySmall.fontSize,
|
||||
caption: text.caption.fontSize,
|
||||
},
|
||||
text,
|
||||
fontFamily,
|
||||
radius,
|
||||
layout,
|
||||
elevation,
|
||||
shadows: {
|
||||
soft: elevation.level1,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const lightTheme: FintechTheme = buildTheme(light, 'light');
|
||||
export const darkTheme: FintechTheme = buildTheme(dark, 'dark');
|
||||
|
||||
/** Both themes by name, for the provider to select from. */
|
||||
export const themes: Record<ThemeName, FintechTheme> = {
|
||||
light: lightTheme,
|
||||
dark: darkTheme,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user