@oxyhq/bloom 0.73.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/lib/commonjs/button/Button.js +104 -42
  2. package/lib/commonjs/button/Button.js.map +1 -1
  3. package/lib/commonjs/button/Button.web.js +11 -3
  4. package/lib/commonjs/button/Button.web.js.map +1 -1
  5. package/lib/commonjs/design-tokens/theme.css +22 -0
  6. package/lib/commonjs/fab/Fab.js +9 -0
  7. package/lib/commonjs/fab/Fab.js.map +1 -1
  8. package/lib/commonjs/fab/Fab.web.js +11 -0
  9. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  10. package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
  11. package/lib/commonjs/theme/build-theme-from-seed.js +7 -5
  12. package/lib/commonjs/theme/build-theme-from-seed.js.map +1 -1
  13. package/lib/commonjs/theme/build-theme.js +9 -4
  14. package/lib/commonjs/theme/build-theme.js.map +1 -1
  15. package/lib/commonjs/theme/color-policy.js +315 -0
  16. package/lib/commonjs/theme/color-policy.js.map +1 -0
  17. package/lib/commonjs/theme/color-scope/seed-scope.js +13 -1
  18. package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
  19. package/lib/commonjs/theme/color-scope/style-builder.js +6 -0
  20. package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
  21. package/lib/commonjs/theme/preset-vars.js +9 -1
  22. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  23. package/lib/commonjs/theme/token-registry.js +11 -1
  24. package/lib/commonjs/theme/token-registry.js.map +1 -1
  25. package/lib/module/button/Button.js +104 -42
  26. package/lib/module/button/Button.js.map +1 -1
  27. package/lib/module/button/Button.web.js +11 -3
  28. package/lib/module/button/Button.web.js.map +1 -1
  29. package/lib/module/design-tokens/theme.css +22 -0
  30. package/lib/module/fab/Fab.js +9 -0
  31. package/lib/module/fab/Fab.js.map +1 -1
  32. package/lib/module/fab/Fab.web.js +11 -0
  33. package/lib/module/fab/Fab.web.js.map +1 -1
  34. package/lib/module/theme/BloomThemeProvider.js.map +1 -1
  35. package/lib/module/theme/build-theme-from-seed.js +7 -5
  36. package/lib/module/theme/build-theme-from-seed.js.map +1 -1
  37. package/lib/module/theme/build-theme.js +9 -4
  38. package/lib/module/theme/build-theme.js.map +1 -1
  39. package/lib/module/theme/color-policy.js +310 -0
  40. package/lib/module/theme/color-policy.js.map +1 -0
  41. package/lib/module/theme/color-scope/seed-scope.js +13 -1
  42. package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
  43. package/lib/module/theme/color-scope/style-builder.js +6 -0
  44. package/lib/module/theme/color-scope/style-builder.js.map +1 -1
  45. package/lib/module/theme/preset-vars.js +9 -1
  46. package/lib/module/theme/preset-vars.js.map +1 -1
  47. package/lib/module/theme/token-registry.js +11 -1
  48. package/lib/module/theme/token-registry.js.map +1 -1
  49. package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/button/types.d.ts +8 -0
  52. package/lib/typescript/commonjs/button/types.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/fab/types.d.ts +6 -1
  56. package/lib/typescript/commonjs/fab/types.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/icons/common.d.ts +1 -1
  58. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/theme/color-policy.d.ts +48 -0
  62. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/theme/token-registry.d.ts +1 -1
  67. package/lib/typescript/commonjs/theme/token-registry.d.ts.map +1 -1
  68. package/lib/typescript/module/button/Button.d.ts.map +1 -1
  69. package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
  70. package/lib/typescript/module/button/types.d.ts +8 -0
  71. package/lib/typescript/module/button/types.d.ts.map +1 -1
  72. package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
  73. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  74. package/lib/typescript/module/fab/types.d.ts +6 -1
  75. package/lib/typescript/module/fab/types.d.ts.map +1 -1
  76. package/lib/typescript/module/icons/common.d.ts +1 -1
  77. package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
  78. package/lib/typescript/module/theme/build-theme-from-seed.d.ts.map +1 -1
  79. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  80. package/lib/typescript/module/theme/color-policy.d.ts +48 -0
  81. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -0
  82. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
  83. package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
  84. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  85. package/lib/typescript/module/theme/token-registry.d.ts +1 -1
  86. package/lib/typescript/module/theme/token-registry.d.ts.map +1 -1
  87. package/package.json +1 -1
  88. package/src/__tests__/Button.test.tsx +166 -1
  89. package/src/__tests__/Button.web.test.tsx +16 -0
  90. package/src/__tests__/preset-vars.test.ts +27 -15
  91. package/src/button/Button.tsx +123 -40
  92. package/src/button/Button.web.tsx +11 -3
  93. package/src/button/types.ts +8 -0
  94. package/src/design-tokens/theme.css +22 -0
  95. package/src/fab/Fab.tsx +3 -0
  96. package/src/fab/Fab.web.tsx +3 -0
  97. package/src/fab/types.ts +6 -1
  98. package/src/theme/BloomThemeProvider.tsx +7 -4
  99. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +1300 -312
  100. package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +442 -442
  101. package/src/theme/__tests__/policy-legibility.test.ts +118 -0
  102. package/src/theme/__tests__/theme-colors-parity.test.ts +11 -6
  103. package/src/theme/build-theme-from-seed.ts +7 -4
  104. package/src/theme/build-theme.ts +9 -4
  105. package/src/theme/color-policy.ts +342 -0
  106. package/src/theme/color-scope/seed-scope.ts +13 -1
  107. package/src/theme/color-scope/style-builder.ts +6 -0
  108. package/src/theme/preset-vars.ts +10 -1
  109. package/src/theme/token-registry.ts +32 -0
  110. package/lib/commonjs/fonts/font-data.web.js +0 -22
  111. package/lib/commonjs/fonts/font-data.web.js.map +0 -1
  112. package/lib/module/fonts/font-data.web.js +0 -18
  113. package/lib/module/fonts/font-data.web.js.map +0 -1
  114. package/lib/typescript/commonjs/fonts/font-data.web.d.ts +0 -5
  115. package/lib/typescript/commonjs/fonts/font-data.web.d.ts.map +0 -1
  116. package/lib/typescript/module/fonts/font-data.web.d.ts +0 -5
  117. package/lib/typescript/module/fonts/font-data.web.d.ts.map +0 -1
  118. package/src/fonts/font-data.web.ts +0 -15
@@ -56,11 +56,23 @@ describe('getPresetVars (engine-backed)', () => {
56
56
  }
57
57
  });
58
58
 
59
- it('drives sidebar-primary / ring from the engine primary role', () => {
59
+ // `--ring` follows the TEXT accent, not the fill. A focus ring is drawn on the
60
+ // page next to a control, so it needs the tone that is legible there — which is
61
+ // the job `--primary-text` exists for. Equating it with `--primary` was the same
62
+ // conflation that made the fill go pastel to stay readable as text.
63
+ it('drives sidebar-primary / ring from the legible brand accent', () => {
60
64
  const vars = getPresetVars('blue', 'light');
61
- expect(vars['--sidebar-primary']).toBe(vars['--primary']);
62
- expect(vars['--ring']).toBe(vars['--primary']);
63
- expect(vars['--chart-1']).toBe(vars['--primary']);
65
+ expect(vars['--ring']).toBe(vars['--primary-text']);
66
+ expect(vars['--sidebar-primary']).toBe(vars['--primary-text']);
67
+ });
68
+
69
+ // The chart ramp is no longer drawn from the primary/secondary/tertiary trio,
70
+ // which spans ~35 degrees of hue and left adjacent series indistinguishable
71
+ // (minimum ΔE 6.2 across the presets). Five hues, evenly spread, one tone.
72
+ it('spreads the chart ramp across distinct hues', () => {
73
+ const vars = getPresetVars('blue', 'light');
74
+ const series = [1, 2, 3, 4, 5].map((i) => vars[`--chart-${i}`]);
75
+ expect(new Set(series).size).toBe(series.length);
64
76
  });
65
77
 
66
78
  it('makes secondary a real contrast color, not a mirror of primary (the fix)', () => {
@@ -72,19 +84,19 @@ describe('getPresetVars (engine-backed)', () => {
72
84
  }
73
85
  });
74
86
 
75
- it('makes card the lightest surface in light mode, darkest in dark (the fix)', () => {
76
- const channels = (rgb: string): number[] =>
77
- (rgb.match(/\d+/g) ?? []).map(Number);
78
- const sum = (rgb: string): number => channels(rgb).reduce((a, b) => a + b, 0);
87
+ // Deliberately inverted from what this asserted before: M3's
88
+ // `surfaceContainerLowest` is the RECESSED step, so in dark a card SANK into
89
+ // the page rather than lifting off it — on all thirteen presets, which is what
90
+ // made it a mapping problem and not a seed one.
91
+ it('makes card lift off the background in BOTH modes', () => {
92
+ const sum = (rgb: string): number =>
93
+ (rgb.match(/\d+/g) ?? []).map(Number).reduce((a, b) => a + b, 0);
79
94
 
80
95
  for (const name of APP_COLOR_NAMES) {
81
- const light = getPresetVars(name, 'light');
82
- // card (surfaceContainerLowest) must be lighter than background in light.
83
- expect(sum(light['--card'] ?? '')).toBeGreaterThan(sum(light['--background'] ?? ''));
84
-
85
- const dark = getPresetVars(name, 'dark');
86
- // card must be darker than (or equal to) background in dark — correct inversion.
87
- expect(sum(dark['--card'] ?? '')).toBeLessThanOrEqual(sum(dark['--background'] ?? ''));
96
+ for (const mode of ['light', 'dark'] as const) {
97
+ const vars = getPresetVars(name, mode);
98
+ expect(sum(vars['--card'] ?? '')).toBeGreaterThan(sum(vars['--background'] ?? ''));
99
+ }
88
100
  }
89
101
  });
90
102
 
@@ -1,4 +1,4 @@
1
- import React, { useMemo, memo } from 'react';
1
+ import React, { useMemo, memo, type ComponentType } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
4
  Animated,
@@ -7,9 +7,12 @@ import {
7
7
  StyleSheet,
8
8
  Text,
9
9
  View,
10
+ type PressableProps,
11
+ type StyleProp,
10
12
  type TextStyle,
11
13
  type ViewStyle,
12
14
  } from 'react-native';
15
+ import { styled } from 'react-native-css';
13
16
 
14
17
  import { useTheme } from '../theme/use-theme';
15
18
  import { usePressAnimation } from '../hooks/usePressAnimation';
@@ -69,6 +72,73 @@ const PILL_RADIUS = 999;
69
72
  const PRESS_SCALE = 0.97;
70
73
  const SCALE_VARIANTS = new Set<string>(['primary', 'secondary', 'inverse']);
71
74
 
75
+ // ---------------------------------------------------------------------------
76
+ // The button renders ONE node — the same shape the web fork renders (one
77
+ // `<button>`).
78
+ //
79
+ // It used to be two: an unstyled `Animated.View` holding the press-scale
80
+ // transform, wrapping the `Pressable` that carried `className`, `style` and
81
+ // every visual. That wrapper — not the pressable — was the flex child of
82
+ // whatever laid the button out, and it hugged its content, so a caller's
83
+ // `className="flex-1"` (and `style={{ alignSelf }}`, and every other layout
84
+ // declaration) applied INSIDE a box that never grew. Nothing errored; the same
85
+ // call site behaved on web and did nothing on native, and consumers worked
86
+ // around it by wrapping every Bloom `Button` in their own `<View>`.
87
+ //
88
+ // Collapsing the two nodes settles "which node does a class belong on?" by
89
+ // removing the choice: layout and visuals are one box here exactly as they are
90
+ // in the DOM, so a layout class and a visual class cannot land in different
91
+ // places. The touch target now scales with the press (0.97 — ~1px on a 40dp
92
+ // button, and the press is registered before it starts), the same trade
93
+ // `PressableScale` made when it dropped its own two-node structure.
94
+ //
95
+ // `styled()` comes from Bloom's own `react-native-css` dependency rather than
96
+ // passing `className` as a bare prop and hoping the consumer's NativeWind
97
+ // interop recognises a component Bloom built at module scope. Same reasoning,
98
+ // and the same silent failure if skipped, as `typography/styled-text.ts`.
99
+ //
100
+ // Both wrappers are built ONCE at module scope so the element type is stable
101
+ // across renders — one built during render remounts the subtree every time.
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /**
105
+ * Exactly the prop surface `Button` hands to the pressable, and no wider.
106
+ * `Animated.createAnimatedComponent` maps `WithAnimatedValue` over every prop of
107
+ * the component it wraps, and doing that to the whole of `PressableProps`
108
+ * overflows the checker (`TS2590: union type that is too complex to represent`).
109
+ * Narrowing is also honest: the function form of `style` is deliberately
110
+ * excluded — see the `useInteractionState` comment in the component body for why
111
+ * Bloom never uses it.
112
+ */
113
+ type ButtonPressableProps = Pick<
114
+ PressableProps,
115
+ | 'accessibilityHint'
116
+ | 'accessibilityLabel'
117
+ | 'accessibilityRole'
118
+ | 'accessibilityState'
119
+ | 'children'
120
+ | 'className'
121
+ | 'disabled'
122
+ | 'hitSlop'
123
+ | 'onPress'
124
+ | 'onPressIn'
125
+ | 'onPressOut'
126
+ | 'testID'
127
+ > & { style?: StyleProp<ViewStyle> };
128
+
129
+ /**
130
+ * The same `Pressable` object, typed down to {@link ButtonPressableProps}.
131
+ * `styled()` derives its mapping type from a dot-path union over the wrapped
132
+ * component's ENTIRE prop type, and the one over `PressableProps` also overflows
133
+ * the checker. Nothing changes at runtime — this is the identical component.
134
+ */
135
+ const ButtonPressable: ComponentType<ButtonPressableProps> = Pressable;
136
+
137
+ const StyledPressable: ComponentType<ButtonPressableProps> = styled(ButtonPressable, {
138
+ className: 'style',
139
+ });
140
+ const AnimatedPressable = Animated.createAnimatedComponent(StyledPressable);
141
+
72
142
  const ButtonComponent: React.FC<ButtonProps> = ({
73
143
  onPress,
74
144
  children,
@@ -170,6 +240,16 @@ const ButtonComponent: React.FC<ButtonProps> = ({
170
240
  styles.borderRadius = PILL_RADIUS;
171
241
  break;
172
242
  case 'icon':
243
+ // Resolved tokens, not `className="bg-background border border-border"`.
244
+ // The class form made the caller's `className` and the icon's own chrome
245
+ // compete for one slot — `<IconButton className="flex-1" />` replaced the
246
+ // default and shipped a transparent, borderless icon button — and it left
247
+ // the chrome dependent on the consumer's Tailwind pipeline resolving
248
+ // Bloom's color names. These are the SAME tokens `Button.web.tsx`'s
249
+ // `resolveVariantStyle('icon')` already writes inline.
250
+ styles.backgroundColor = theme.colors.background;
251
+ styles.borderWidth = 1;
252
+ styles.borderColor = theme.colors.border;
173
253
  styles.borderRadius = PILL_RADIUS;
174
254
  styles.padding = 8;
175
255
  styles.width = sizeConfig.minHeight;
@@ -220,7 +300,6 @@ const ButtonComponent: React.FC<ButtonProps> = ({
220
300
 
221
301
  const defaultHitSlop = variant === 'icon' ? ICON_HIT_SLOP : undefined;
222
302
  const resolvedActiveOpacity = activeOpacity ?? (variant === 'icon' ? 0.7 : 0.8);
223
- const resolvedClassName = className ?? (variant === 'icon' ? 'bg-background border border-border' : undefined);
224
303
 
225
304
  const content = (
226
305
  <>
@@ -233,44 +312,48 @@ const ButtonComponent: React.FC<ButtonProps> = ({
233
312
  );
234
313
 
235
314
  return (
236
- <Animated.View style={hasScaleFeedback ? { transform: [{ scale: scaleAnim }] } : undefined}>
237
- <Pressable
238
- {...(resolvedClassName ? { className: resolvedClassName } as Record<string, string> : {})}
239
- style={[
240
- baseStyles,
241
- disabled && !loading && { opacity: 0.5 },
242
- pressed && !hasScaleFeedback && !isInteractionBlocked && { opacity: resolvedActiveOpacity },
243
- style,
244
- ]}
245
- onPress={isInteractionBlocked ? undefined : onPress}
246
- onPressIn={handlePressIn}
247
- onPressOut={handlePressOut}
248
- disabled={isInteractionBlocked}
249
- hitSlop={hitSlop ?? defaultHitSlop}
250
- accessibilityLabel={accessibilityLabel}
251
- accessibilityHint={accessibilityHint}
252
- accessibilityRole="button"
253
- accessibilityState={loading ? { disabled: isInteractionBlocked, busy: true } : { disabled: isInteractionBlocked }}
254
- testID={testID}
255
- >
256
- {loading ? (
257
- <>
258
- <View
259
- style={styles.loadingHiddenContent}
260
- importantForAccessibility="no-hide-descendants"
261
- accessibilityElementsHidden
262
- >
263
- {content}
264
- </View>
265
- <View style={styles.loadingOverlay}>
266
- <ActivityIndicator size="small" color={loadingColor ?? resolvedTextColor} />
267
- </View>
268
- </>
269
- ) : (
270
- content
271
- )}
272
- </Pressable>
273
- </Animated.View>
315
+ <AnimatedPressable
316
+ className={className}
317
+ style={[
318
+ baseStyles,
319
+ disabled && !loading && { opacity: 0.5 },
320
+ pressed && !hasScaleFeedback && !isInteractionBlocked && { opacity: resolvedActiveOpacity },
321
+ // Before the caller's `style`, so `style` keeps winning the whole array
322
+ // the way it always has (and the way the web fork spreads it last). The
323
+ // cost is that a caller who sets `transform` on a scale variant replaces
324
+ // the press scale instead of composing with it — when the transform sat
325
+ // on a separate wrapper node the two multiplied.
326
+ hasScaleFeedback ? { transform: [{ scale: scaleAnim }] } : null,
327
+ style,
328
+ ]}
329
+ onPress={isInteractionBlocked ? undefined : onPress}
330
+ onPressIn={handlePressIn}
331
+ onPressOut={handlePressOut}
332
+ disabled={isInteractionBlocked}
333
+ hitSlop={hitSlop ?? defaultHitSlop}
334
+ accessibilityLabel={accessibilityLabel}
335
+ accessibilityHint={accessibilityHint}
336
+ accessibilityRole="button"
337
+ accessibilityState={loading ? { disabled: isInteractionBlocked, busy: true } : { disabled: isInteractionBlocked }}
338
+ testID={testID}
339
+ >
340
+ {loading ? (
341
+ <>
342
+ <View
343
+ style={styles.loadingHiddenContent}
344
+ importantForAccessibility="no-hide-descendants"
345
+ accessibilityElementsHidden
346
+ >
347
+ {content}
348
+ </View>
349
+ <View style={styles.loadingOverlay}>
350
+ <ActivityIndicator size="small" color={loadingColor ?? resolvedTextColor} />
351
+ </View>
352
+ </>
353
+ ) : (
354
+ content
355
+ )}
356
+ </AnimatedPressable>
274
357
  );
275
358
  };
276
359
 
@@ -18,9 +18,17 @@ import type { ButtonProps, ButtonSize, ButtonVariant } from './types';
18
18
  export type { ButtonProps, ButtonVariant, ButtonSize } from './types';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
- // Geometry — mirrors the native (`Button.tsx`) impl exactly so the web button
22
- // is pixel-consistent with native. Sizes below are the resolved primitives;
23
- // the shadcn-style aliases (`sm | md | lg | icon`) are normalized first.
21
+ // Geometry — mirrors the native (`Button.tsx`) impl so the web button is
22
+ // pixel-consistent with native. Sizes below are the resolved primitives; the
23
+ // shadcn-style aliases (`sm | md | lg | icon`) are normalized first.
24
+ //
25
+ // ONE deliberate exception, in `containerStyle`: `text`/`link` clear
26
+ // `minHeight` here so the control hugs its label and sits inline (which is what
27
+ // makes `asChild` anchors work), while native keeps the size config's value to
28
+ // preserve the touch target. The 4/8 padding override those variants get is NOT
29
+ // an exception — native applies exactly the same one, so `text` is a compact
30
+ // affordance and `ghost` a full-size button without a background on both
31
+ // platforms. Only the variant COLOR table below is shared between them.
24
32
  // ---------------------------------------------------------------------------
25
33
 
26
34
  type NativeSize = 'small' | 'medium' | 'large';
@@ -11,6 +11,14 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
11
11
  * the set. On native they fall back to a sensible equivalent
12
12
  * (`outline → secondary`, `link → text`, `destructive → primary` tinted with
13
13
  * the negative token) so passing them never crashes a native consumer.
14
+ *
15
+ * `text` (and its web alias `link`) is DELIBERATELY not a transparent `ghost`:
16
+ * it is the compact inline affordance, so it overrides the size config's padding
17
+ * down to 4/8 on BOTH platforms. `ghost` keeps the full padding — it is a
18
+ * regular button that happens to have no background. The one geometry that does
19
+ * differ by platform is `minHeight`: web clears it for `text`/`link` so the
20
+ * control hugs its label and sits inline, native keeps the size config's value
21
+ * because a finger needs the target and a cursor does not.
14
22
  */
15
23
  export type ButtonVariant =
16
24
  | 'primary'
@@ -62,6 +62,28 @@
62
62
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
63
63
  --color-sidebar-border: var(--sidebar-border);
64
64
  --color-sidebar-ring: var(--sidebar-ring);
65
+ --color-primary-text: var(--primary-text);
66
+ --color-primary-subtle: var(--primary-subtle);
67
+ --color-secondary-text: var(--secondary-text);
68
+ --color-secondary-subtle: var(--secondary-subtle);
69
+ --color-tertiary-text: var(--tertiary-text);
70
+ --color-tertiary-subtle: var(--tertiary-subtle);
71
+ --color-success: var(--success);
72
+ --color-success-foreground: var(--success-foreground);
73
+ --color-success-text: var(--success-text);
74
+ --color-success-subtle: var(--success-subtle);
75
+ --color-error: var(--error);
76
+ --color-error-foreground: var(--error-foreground);
77
+ --color-error-text: var(--error-text);
78
+ --color-error-subtle: var(--error-subtle);
79
+ --color-warning: var(--warning);
80
+ --color-warning-foreground: var(--warning-foreground);
81
+ --color-warning-text: var(--warning-text);
82
+ --color-warning-subtle: var(--warning-subtle);
83
+ --color-info: var(--info);
84
+ --color-info-foreground: var(--info-foreground);
85
+ --color-info-text: var(--info-text);
86
+ --color-info-subtle: var(--info-subtle);
65
87
  --color-destructive-foreground: #ffffff;
66
88
  --color-divider: var(--border);
67
89
  --spacing-space-2: 2px;
package/src/fab/Fab.tsx CHANGED
@@ -206,6 +206,9 @@ function resolveVariant(
206
206
  ): { background: string; foreground: string } {
207
207
  switch (variant) {
208
208
  case 'secondary':
209
+ return { background: colors.secondary, foreground: colors.secondaryForeground };
210
+ case 'tertiary':
211
+ return { background: colors.tertiary, foreground: colors.tertiaryForeground };
209
212
  case 'surface':
210
213
  return { background: colors.card, foreground: colors.text };
211
214
  case 'primary':
@@ -166,6 +166,9 @@ function resolveVariant(
166
166
  ): { background: string; foreground: string; ring: string } {
167
167
  switch (variant) {
168
168
  case 'secondary':
169
+ return { background: c.secondary, foreground: c.secondaryForeground, ring: c.secondary };
170
+ case 'tertiary':
171
+ return { background: c.tertiary, foreground: c.tertiaryForeground, ring: c.tertiary };
169
172
  case 'surface':
170
173
  return { background: c.card, foreground: c.text, ring: c.primary };
171
174
  case 'primary':
package/src/fab/types.ts CHANGED
@@ -9,7 +9,12 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
9
9
  * A lower-emphasis FAB that still reads as elevated.
10
10
  * - `surface` — alias of `secondary`, named for Material-style "surface" FABs.
11
11
  */
12
- export type FabVariant = 'primary' | 'secondary' | 'surface';
12
+ /**
13
+ * M3 defines four FAB colour variants. Bloom previously mapped `secondary` onto
14
+ * the neutral card surface, so a "secondary FAB" was a grey circle and the
15
+ * accent trio had no home in the component that most wants one.
16
+ */
17
+ export type FabVariant = 'primary' | 'secondary' | 'tertiary' | 'surface';
13
18
 
14
19
  /**
15
20
  * FAB size presets, matching Material-ish conventions but in Bloom's scale:
@@ -410,10 +410,13 @@ export function BloomThemeProvider({
410
410
  const themeColors = useMemo(
411
411
  () =>
412
412
  effectiveSeed
413
- ? buildThemeFromSeed(effectiveSeed, resolved, undefined, undefined, {
414
- secondarySeed: effectiveSecondary,
415
- tertiarySeed: effectiveTertiary,
416
- })
413
+ ? buildThemeFromSeed(
414
+ effectiveSeed,
415
+ resolved,
416
+ undefined,
417
+ undefined,
418
+ { secondarySeed: effectiveSecondary, tertiarySeed: effectiveTertiary }
419
+ )
417
420
  : buildTheme(colorPreset, resolved, isAdaptive, explicitAccents),
418
421
  [
419
422
  effectiveSeed,