MetaMask / MetaMask/metamask-design-system

Add design-token shadow utilities to @metamask/design-system-twrnc-preset

Open
#1,288 0 comments 0 reactions 1 assignee Claimed by @georgewrmarshall View on GitHub
enhancement team-design-system
Dominant language
TypeScript
Stars
37
Forks
14
Avg merge
1d 9h
Merged PRs (30d)
60

Description

## Summary

Add **design-token-backed shadow utilities** to `@metamask/design-system-twrnc-preset` so React Native components can use `tw.style('shadow-lg')` instead of importing `@metamask/design-tokens` and spreading inline React Native shadow style objects.

**Issue type:** Enhancement
**Primary package:** `@metamask/design-system-twrnc-preset`
**Pilot consumer:** `BottomSheetDialog` in `@metamask/design-system-react-native`

---

## Problem

### Current state

1. **`twrnc` supports shadows**, but its built-in `shadow-lg` etc. use **generic defaults** that do **not** match MMDS token values.
2. **`generateTailwindConfig`** in the twrnc preset only wires **colors** and **typography** — no shadows.
3. **`BottomSheetDialog`** is the only RN component importing theme objects solely for shadows:

```tsx
// packages/design-system-react-native/src/components/BottomSheetDialog/BottomSheetDialog.tsx
import { lightTheme, resolveDarkTheme } from '@metamask/design-tokens';

const shadowLg =
currentTheme === Theme.Light
? lightTheme.shadows.size.lg
: resolveDarkTheme(isPureBlack).shadows.size.lg;

// later in sheetStyle:
{ ...shadowLg } // spread into style array
```

4. Component READMEs already document `twClassName="shadow-lg"` (Button, ButtonIcon, BannerAlert, HeaderSearch), but **without preset support those classes would not match design tokens**.

### Why inline spreads exist

React Native shadows are **style props**, not CSS:

```ts
{
shadowColor: string;
shadowOffset: { width: number; height: number };
shadowOpacity: number;
shadowRadius: number;
}
```

`twrnc` does not parse CSS `box-shadow` strings. It expects either low-level utilities (`shadow-radius-10`, `shadow-black`) or **custom utilities** registered via Tailwind `plugin()` that output RN style objects. See [twrnc box-shadow docs](https://github.com/jaredh159/twrnc#box-shadows).

---

## Goal

After this work:

```tsx
// ✅ Target pattern
const tw = useTailwind();

tw.style('shadow-lg bg-alternative rounded-t-3xl');
// shadow-lg resolves to lightTheme.shadows.size.lg OR darkTheme.shadows.size.lg
// based on active ThemeProvider theme (and isPureBlack for dark — same shadow values today)
```

```tsx
// ❌ Remove from BottomSheetDialog
import { lightTheme, resolveDarkTheme } from '@metamask/design-tokens';
const shadowLg = ...;
{ ...shadowLg }
```

---

## Token source of truth

### Type definition

`packages/design-tokens/src/js/themes/types.ts`:

```ts
export type ThemeShadows = {
size: {
xs: ShadowShape;
sm: ShadowShape;
md: ShadowShape;
lg: ShadowShape;
};
};
```

### Theme exports

| Theme | Shadows file | Notes |
|-------|--------------|-------|
| Light | `packages/design-tokens/src/js/themes/lightTheme/shadows.ts` | Uses `colors.shadow.default` = `#0000001a` |
| Dark (grey) | `packages/design-tokens/src/js/themes/darkTheme/shadows.ts` | Uses `colors.shadow.default` = `#00000066` |
| Pure black dark | Inherits from `darkTheme` via merge | **No shadow deltas today** — `pureBlackDarkTheme.shadows === darkTheme.shadows` |

Use `resolveDarkTheme(isPureBlack).shadows` for dark-mode shadow lookup (same pattern as colors).

### Reference values (all sizes share offset `{ width: 0, height: 2 }`, `shadowOpacity: 1`)

| Size | Light `shadowRadius` | Light `shadowColor` | Dark `shadowRadius` | Dark `shadowColor` |
|------|---------------------|---------------------|---------------------|-------------------|
| xs | 4 | `#0000001a` | 4 | `#00000066` |
| sm | 8 | `#0000001a` | 8 | `#00000066` |
| md | 16 | `#0000001a` | 16 | `#00000066` |
| lg | 40 | `#0000001a` | 40 | `#00000066` |

### MMDS `lg` vs twrnc default `shadow-lg`

| Property | MMDS token `lg` (dark) | twrnc built-in `shadow-lg` |
|----------|------------------------|----------------------------|
| `shadowOffset` | `{ width: 0, height: 2 }` | `{ width: 1, height: 1 }` |
| `shadowColor` | `#00000066` | `#000` |
| `shadowOpacity` | `1` | `0.15` |
| `shadowRadius` | `40` | `8` |
| `elevation` | **not in tokens** | `8` (Android) |

**Important:** MMDS shadow tokens do **not** include `elevation`. Do **not** add `elevation` to custom utilities unless design explicitly requests it — match token objects exactly so `BottomSheetDialog` behavior is unchanged.

---

## Architecture (how it should work)

```
ThemeProvider(theme, isPureBlack)
└─ generateTailwindConfig(theme, isPureBlack)
├─ getThemeColors(...) ← existing
└─ getThemeShadows(...) ← NEW
└─ tailwind plugin addUtilities({
'.shadow-xs': { ...theme.shadows.size.xs },
'.shadow-sm': { ... },
'.shadow-md': { ... },
'.shadow-lg': { ... },
})
└─ create(config) → tw
└─ tw`shadow-lg` → RN shadow props from tokens
```

Custom utilities **override** twrnc's built-in `shadow-*` classes when registered with the same names ([twrnc docs](https://github.com/jaredh159/twrnc#box-shadows)).

---

## Implementation plan (for agent)

### Step 1 — Add shadow resolver (mirror `colors.ts` pattern)

**File:** `packages/design-system-twrnc-preset/src/shadows.ts` (new)

Follow the pattern in `packages/design-system-twrnc-preset/src/colors.ts`:

```ts
import {
darkTheme,
lightTheme,
resolveDarkTheme,
} from '@metamask/design-tokens';
import type { ThemeShadows } from '@metamask/design-tokens'; // or import from themes/types if exported

import { Theme } from './Theme.types';

export const getThemeShadows = (
theme: Theme,
isPureBlack = false,
): ThemeShadows => {
if (theme === Theme.Light) {
return lightTheme.shadows;
}
return resolveDarkTheme(isPureBlack).shadows;
};
```

Export from `packages/design-system-twrnc-preset/src/index.ts` if useful for consumers (optional).

### Step 2 — Register shadow utilities in Tailwind config

**File:** `packages/design-system-twrnc-preset/src/tailwind.config.ts`

1. Import `plugin` from `tailwindcss/plugin` (same as twrnc readme examples).
2. Import `getThemeShadows`.
3. Add a `plugins` array to the returned config:

```ts
import plugin from 'tailwindcss/plugin';
import { getThemeShadows } from './shadows';

export const generateTailwindConfig = (theme: Theme, isPureBlack = false): TwConfig => {
const shadows = getThemeShadows(theme, isPureBlack);

return {
theme: { /* existing colors + typography */ },
plugins: [
plugin(({ addUtilities }) => {
addUtilities({
'.shadow-xs': shadows.size.xs,
'.shadow-sm': shadows.size.sm,
'.shadow-md': shadows.size.md,
'.shadow-lg': shadows.size.lg,
});
}),
],
};
};
```

**Verify:** `ThemeProvider` already calls `generateTailwindConfig(theme, isPureBlack)` and recreates `tw` on theme change — shadow utilities should update automatically. No `ThemeProvider` changes expected unless testing reveals otherwise.

### Step 3 — Migrate `BottomSheetDialog`

**File:** `packages/design-system-react-native/src/components/BottomSheetDialog/BottomSheetDialog.tsx`

1. Remove imports: `lightTheme`, `resolveDarkTheme` from `@metamask/design-tokens`.
2. Remove: `useTheme`, `usePureBlack` if only used for shadow selection (keep `usePureBlack` if still needed for `bg-alternative` vs `bg-default`).
3. Remove: `shadowLg` useMemo variable and `...shadowLg` spread.
4. Add `'shadow-lg'` to the existing `tw.style(...)` call:

```tsx
tw.style(
isPureBlack ? 'bg-alternative' : 'bg-default',
'rounded-t-3xl overflow-hidden border border-muted shadow-lg',
twClassName,
)
```

5. Update `useMemo` dependency array — remove `shadowLg`, `currentTheme` if no longer needed for shadows.

**File:** `packages/design-system-react-native/src/components/BottomSheetDialog/BottomSheetDialog.test.tsx`

- Update mocks if they assert on shadow styles; ensure tests still pass with twrnc shadow utility.

### Step 4 — Tests

**File:** `packages/design-system-twrnc-preset/src/shadows.test.ts` (new — package has jest but no tests yet; see TODO in `jest.config.js`)

Suggested cases:

```ts
describe('getThemeShadows', () => {
it('returns light theme shadows for Theme.Light');
it('returns dark theme shadows for Theme.Dark + isPureBlack false');
it('returns same dark shadows for Theme.Dark + isPureBlack true (no shadow deltas)');
});

describe('generateTailwindConfig shadow utilities', () => {
it('tw.style("shadow-lg") matches lightTheme.shadows.size.lg for light config');
it('tw.style("shadow-lg") matches darkTheme.shadows.size.lg for dark config');
});
```

Use `create(generateTailwindConfig(...))` from `twrnc` in tests (same as runtime).

**Coverage:** `jest.config.js` has thresholds (84% lines). New tests should satisfy or improve coverage.

### Step 5 — Documentation

**File:** `packages/design-system-twrnc-preset/README.md`

Add section **Shadow utilities**:

- Available classes: `shadow-xs`, `shadow-sm`, `shadow-md`, `shadow-lg`
- Theme-aware via `ThemeProvider`
- Example with `tw.style('shadow-lg')`
- Note that these override twrnc defaults and match `@metamask/design-tokens`

---

## Verification checklist (run from repo root)

```bash
yarn build
yarn workspace @metamask/design-system-twrnc-preset run test
yarn workspace @metamask/design-system-react-native run test -- BottomSheetDialog
```

Manual (optional):

```bash
yarn storybook:ios
# Open BottomSheetDialog story — sheet should still have visible elevation/shadow
# Toggle light / dark / pureBlack backgrounds in preview
```

---

## Acceptance criteria

- [ ] `shadow-xs|sm|md|lg` utilities in twrnc preset resolve to correct **token** values for light and dark themes
- [ ] Shadow utilities update when `ThemeProvider` switches `theme` or `isPureBlack`
- [ ] Custom utilities **override** twrnc built-in shadow defaults (token-accurate, not generic)
- [ ] `BottomSheetDialog` no longer imports `@metamask/design-tokens` for shadows
- [ ] `BottomSheetDialog` uses `shadow-lg` via `tw.style()`
- [ ] Unit tests added in `@metamask/design-system-twrnc-preset`
- [ ] README updated
- [ ] `yarn build` passes from monorepo root
- [ ] No `elevation` added unless explicitly aligned with design (tokens omit it)

---

## Out of scope

- Changing shadow **token values** in `@metamask/design-tokens`
- Adding pure-black-specific shadow overrides (none exist today)
- Web React / CSS `shadow-lg` (already in `packages/design-tokens/src/tailwind/theme.css`)
- Migrating every README example that mentions `shadow-lg` (only `BottomSheetDialog` is required; others can follow in separate PRs)
- `shadow-primary`, `shadow-error` semantic shadow colors (tokens define `colors.shadow.primary/error` but no `shadows.size` variants — future work)

---

## Files to touch (expected diff)

| Action | Path |
|--------|------|
| **Create** | `packages/design-system-twrnc-preset/src/shadows.ts` |
| **Create** | `packages/design-system-twrnc-preset/src/shadows.test.ts` |
| **Edit** | `packages/design-system-twrnc-preset/src/tailwind.config.ts` |
| **Edit** | `packages/design-system-twrnc-preset/README.md` |
| **Edit** | `packages/design-system-react-native/src/components/BottomSheetDialog/BottomSheetDialog.tsx` |
| **Edit** | `packages/design-system-react-native/src/components/BottomSheetDialog/BottomSheetDialog.test.tsx` (if needed) |

**Do not modify** unless requirements change:

- `packages/design-tokens/**` (shadow token values)
- `ThemeProvider.tsx` (likely unchanged)

---

## Related work

- Epic: #1271 (Pure Black Theme and Provider)
- #1274 — TWRNC `ThemeProvider` + `isPureBlack` (colors; shadows should follow same theme resolution)
- #1281 — `BottomSheetDialog` pure-black surfaces (inline shadows identified as tech debt during that PR)
- #1282 — Draft stack PR (may contain interim `BottomSheetDialog` shadow code to clean up)
- Jira: [TMCU-622](https://consensyssoftware.atlassian.net/browse/TMCU-622)

---

## Agent notes

- **Monorepo commands:** Always run from repo root. Use `yarn workspace @metamask/design-system-twrnc-preset run ...` — never `cd packages/...`.
- **Import rule:** Apps/packages import via package names (`@metamask/design-tokens`), not relative paths into `packages/`.
- **Peer dependency:** `@metamask/design-tokens` is a peer of the twrnc preset — use `lightTheme`, `darkTheme`, `resolveDarkTheme` from that package.
- **Prior art:** `packages/design-system-twrnc-preset/src/colors.ts` + `getThemeColors()` is the exact pattern to mirror for shadows.
- **If `tailwindcss/plugin` import fails:** It is bundled with `twrnc` / tailwind — check `node_modules/twrnc` and existing monorepo tailwind usage; do not add a new dependency without justification.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.