685 lines
28 KiB
TypeScript
685 lines
28 KiB
TypeScript
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;
|
|
infoSurface: string;
|
|
infoBorder: string;
|
|
/** Alias of `danger`, kept because ~30 call sites use it for field errors. */
|
|
error: string;
|
|
|
|
/* ── 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: 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;
|
|
h2: number;
|
|
h3: number;
|
|
body: number;
|
|
bodySmall: number;
|
|
caption: 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: {
|
|
/** @deprecated Use `theme.elevation.level1`, which also carries the border
|
|
* that Android needs in order to show any separation at all. */
|
|
soft: ViewStyle;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
};
|