@titan-design/react-ui 0.4.0 → 0.6.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 (150) hide show
  1. package/dist/MesoStatusCard-CQk71hSi.d.mts +77 -0
  2. package/dist/MesoStatusCard-CQk71hSi.d.ts +77 -0
  3. package/dist/bodymap.d.mts +246 -3
  4. package/dist/bodymap.d.ts +246 -3
  5. package/dist/bodymap.js +232 -185
  6. package/dist/bodymap.js.map +1 -1
  7. package/dist/bodymap.mjs +1963 -11
  8. package/dist/bodymap.mjs.map +1 -1
  9. package/dist/index.d.mts +193 -744
  10. package/dist/index.d.ts +193 -744
  11. package/dist/index.js +1551 -1856
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +10770 -8213
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/pages-D7Jlssvp.d.ts +791 -0
  16. package/dist/pages-MO0JCySL.d.mts +791 -0
  17. package/dist/pages.d.mts +4 -0
  18. package/dist/pages.d.ts +4 -0
  19. package/dist/pages.js +6694 -0
  20. package/dist/pages.js.map +1 -0
  21. package/dist/pages.mjs +6674 -0
  22. package/dist/pages.mjs.map +1 -0
  23. package/dist/theme/index.d.mts +473 -211
  24. package/dist/theme/index.d.ts +473 -211
  25. package/dist/theme/index.js +341 -127
  26. package/dist/theme/index.js.map +1 -1
  27. package/dist/theme/index.mjs +2085 -2
  28. package/dist/theme/index.mjs.map +1 -1
  29. package/dist/theme/tokens-css.js +148 -139
  30. package/dist/theme/tokens-css.js.map +1 -1
  31. package/dist/theme/tokens-css.mjs +582 -1
  32. package/dist/theme/tokens-css.mjs.map +1 -1
  33. package/package.json +11 -7
  34. package/src/components/custom/DateTime/DateTime.stories.tsx +82 -6
  35. package/src/components/custom/DateTime/DateTime.test.tsx +29 -3
  36. package/src/components/custom/DateTime/DateTime.tsx +55 -15
  37. package/src/components/custom/Gauge/Gauge.test.tsx +2 -2
  38. package/src/components/custom/Gauge/Gauge.tsx +6 -3
  39. package/src/components/custom/Scatter/Scatter.tsx +3 -5
  40. package/src/components/custom/Treemap/Treemap.tsx +3 -5
  41. package/src/components/custom/Typography/Typography.stories.tsx +45 -27
  42. package/src/components/custom/Typography/Typography.test.tsx +13 -3
  43. package/src/components/custom/Typography/Typography.tsx +5 -0
  44. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +2 -1
  45. package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
  46. package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
  47. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  48. package/src/components/custom/Workout/BodyMap.tsx +6 -3
  49. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +6 -3
  50. package/src/components/custom/Workout/CapacityBandChart.test.tsx +2 -2
  51. package/src/components/custom/Workout/CapacityBandChart.tsx +9 -6
  52. package/src/components/custom/Workout/DeviationBar.test.tsx +1 -1
  53. package/src/components/custom/Workout/DeviationBar.tsx +7 -4
  54. package/src/components/custom/Workout/ExerciseCard.tsx +5 -1
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +19 -5
  56. package/src/components/custom/Workout/InputBar.tsx +2 -1
  57. package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
  58. package/src/components/custom/Workout/IntensityBar.tsx +113 -58
  59. package/src/components/custom/Workout/MesoCard.tsx +8 -3
  60. package/src/components/custom/Workout/MesoProgressBar.test.tsx +1 -1
  61. package/src/components/custom/Workout/MesoProgressBar.tsx +3 -2
  62. package/src/components/custom/Workout/MesoStatusCard.tsx +20 -13
  63. package/src/components/custom/Workout/MuscleGroupChip.tsx +7 -4
  64. package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -9
  65. package/src/components/custom/Workout/PrBadge.tsx +7 -4
  66. package/src/components/custom/Workout/PrHistoryModal.tsx +6 -5
  67. package/src/components/custom/Workout/ProgramPlanningPage.tsx +2 -1
  68. package/src/components/custom/Workout/README.md +50 -0
  69. package/src/components/custom/Workout/ReadinessCheck.tsx +7 -4
  70. package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
  71. package/src/components/custom/Workout/RestTimer.tsx +50 -44
  72. package/src/components/custom/Workout/SetRow.tsx +12 -2
  73. package/src/components/custom/Workout/Sparkline.tsx +2 -1
  74. package/src/components/custom/Workout/StatusDot.test.tsx +2 -2
  75. package/src/components/custom/Workout/StatusDot.tsx +12 -9
  76. package/src/components/custom/Workout/StrengthTrendChart.tsx +9 -6
  77. package/src/components/custom/Workout/SupersetWrapper.tsx +2 -1
  78. package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
  79. package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
  80. package/src/components/custom/Workout/TempoDisplay.tsx +109 -3
  81. package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
  82. package/src/components/custom/Workout/VelocityStrip.tsx +203 -38
  83. package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
  84. package/src/components/custom/Workout/WeekRow.tsx +3 -2
  85. package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
  86. package/src/components/custom/Workout/WeightBadge.tsx +8 -4
  87. package/src/components/custom/Workout/WorkoutCard.tsx +5 -2
  88. package/src/components/custom/Workout/WorkoutPill.tsx +12 -8
  89. package/src/components/custom/Workout/icons.tsx +7 -0
  90. package/src/components/custom/Workout/index.ts +38 -47
  91. package/src/components/icons/SvgIcon.tsx +50 -0
  92. package/src/components/icons/icons.stories.tsx +57 -0
  93. package/src/components/icons/icons.test.tsx +43 -0
  94. package/src/components/icons/icons.tsx +45 -0
  95. package/src/components/icons/index.ts +4 -0
  96. package/src/components/index.ts +6 -0
  97. package/src/components/shell/BrandLockup.stories.tsx +27 -0
  98. package/src/components/shell/BrandLockup.test.tsx +23 -0
  99. package/src/components/shell/BrandLockup.tsx +47 -0
  100. package/src/components/shell/DeviceIndicator.stories.tsx +28 -0
  101. package/src/components/shell/DeviceIndicator.test.tsx +26 -0
  102. package/src/components/shell/DeviceIndicator.tsx +52 -0
  103. package/src/components/shell/DeviceMenu.stories.tsx +47 -0
  104. package/src/components/shell/DeviceMenu.test.tsx +40 -0
  105. package/src/components/shell/DeviceMenu.tsx +65 -0
  106. package/src/components/shell/DeviceRow.stories.tsx +38 -0
  107. package/src/components/shell/DeviceRow.test.tsx +37 -0
  108. package/src/components/shell/DeviceRow.tsx +58 -0
  109. package/src/components/shell/README.md +86 -0
  110. package/src/components/shell/SessionStatePill.stories.tsx +27 -0
  111. package/src/components/shell/SessionStatePill.test.tsx +25 -0
  112. package/src/components/shell/SessionStatePill.tsx +46 -0
  113. package/src/components/shell/TopBar.stories.tsx +49 -0
  114. package/src/components/shell/TopBar.test.tsx +36 -0
  115. package/src/components/shell/TopBar.tsx +89 -0
  116. package/src/components/shell/index.ts +8 -0
  117. package/src/components/ui/indicator/Indicator.stories.tsx +37 -1
  118. package/src/components/ui/indicator/Indicator.test.tsx +19 -1
  119. package/src/components/ui/indicator/Indicator.tsx +45 -6
  120. package/src/components/ui/indicator/index.ts +1 -1
  121. package/src/components/ui/spinner/Spinner.tsx +3 -2
  122. package/src/components/ui/toolbar-button/ToolbarButton.tsx +2 -1
  123. package/src/index.ts +3 -2
  124. package/src/pages.ts +61 -0
  125. package/src/test/stories-smoke.test.tsx +50 -0
  126. package/src/theme/ColorSystem.stories.tsx +323 -0
  127. package/src/theme/Colors.stories.tsx +69 -1
  128. package/src/theme/barrel.ts +54 -0
  129. package/src/theme/config.ts +16 -3
  130. package/src/theme/extracted-colors-dataviz.ts +21 -0
  131. package/src/theme/extracted-colors-ui.ts +20 -0
  132. package/src/theme/global.css +64 -52
  133. package/src/theme/gradients.test.ts +25 -0
  134. package/src/theme/gradients.ts +36 -0
  135. package/src/theme/index.ts +8 -43
  136. package/src/theme/native-theming-guard.test.ts +117 -0
  137. package/src/theme/tokens/primitives.test.ts +232 -0
  138. package/src/theme/tokens/primitives.ts +247 -0
  139. package/src/theme/tokens/semantic.ts +93 -81
  140. package/src/theme/workout-tokens.ts +22 -21
  141. package/src/utils/avatar-color.ts +3 -3
  142. package/tailwind.config.js +10 -3
  143. package/dist/bodymap-BhG55xHM.d.mts +0 -318
  144. package/dist/bodymap-BhG55xHM.d.ts +0 -318
  145. package/dist/chunk-2SISKTWM.mjs +0 -1027
  146. package/dist/chunk-2SISKTWM.mjs.map +0 -1
  147. package/dist/chunk-7XPB4NAO.mjs +0 -1013
  148. package/dist/chunk-7XPB4NAO.mjs.map +0 -1
  149. package/dist/chunk-SNVKL5WZ.mjs +0 -883
  150. package/dist/chunk-SNVKL5WZ.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { View, Text } from 'react-native'
3
- import { primitiveColors, discreteRainbow } from './tokens/primitives'
3
+ import { primitiveColors, discreteRainbow, primitiveRamps, categoricalPalette } from './tokens/primitives'
4
4
  import { semanticColorsLight, semanticColorsDark } from './tokens/semantic'
5
5
 
6
6
  const meta: Meta = {
@@ -236,6 +236,74 @@ export const DataVisualizationColors: StoryObj = {
236
236
  ),
237
237
  }
238
238
 
239
+ function CategoricalRow({ name, colors }: { name: string; colors: readonly string[] }) {
240
+ return (
241
+ <View style={{ marginBottom: 24 }}>
242
+ <Text className="text-lg font-bold text-text-primary mb-3">
243
+ {name} <Text className="text-text-secondary text-sm">({colors.length})</Text>
244
+ </Text>
245
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
246
+ {colors.map((color, index) => (
247
+ <View key={index} style={{ alignItems: 'center' }}>
248
+ <View
249
+ style={{
250
+ width: 56,
251
+ height: 56,
252
+ borderRadius: 8,
253
+ backgroundColor: color,
254
+ borderWidth: 1,
255
+ borderColor: '#374151',
256
+ }}
257
+ />
258
+ <Text className="text-text-secondary text-xs mt-1">{color.toUpperCase()}</Text>
259
+ </View>
260
+ ))}
261
+ </View>
262
+ </View>
263
+ )
264
+ }
265
+
266
+ export const TonalRamps: StoryObj = {
267
+ render: () => (
268
+ <View style={{ padding: 24 }}>
269
+ <Text className="text-2xl font-bold text-text-primary mb-6">
270
+ Tonal Ramps (Foundations)
271
+ </Text>
272
+ <Text className="text-text-secondary mb-6">
273
+ Seven OKLCH-generated hue ramps, 11 perceptual steps each (50 → 950). Built with hue-torsion and a
274
+ chroma arc; amber absorbs the former yellow (lemon → warm body) and cyan absorbs the former steel.
275
+ </Text>
276
+
277
+ <ColorScale name="Red" colors={primitiveRamps.red} />
278
+ <ColorScale name="Orange" colors={primitiveRamps.orange} />
279
+ <ColorScale name="Amber" colors={primitiveRamps.amber} />
280
+ <ColorScale name="Green" colors={primitiveRamps.green} />
281
+ <ColorScale name="Cyan" colors={primitiveRamps.cyan} />
282
+ <ColorScale name="Blue" colors={primitiveRamps.blue} />
283
+ <ColorScale name="Magenta" colors={primitiveRamps.magenta} />
284
+ </View>
285
+ ),
286
+ }
287
+
288
+ export const CategoricalPalette: StoryObj = {
289
+ render: () => (
290
+ <View style={{ padding: 24 }}>
291
+ <Text className="text-2xl font-bold text-text-primary mb-6">
292
+ Categorical Palette
293
+ </Text>
294
+ <Text className="text-text-secondary mb-6">
295
+ Ordered, colorblind-safe qualitative series (worst-case deut/protanopia floor 8 through the first
296
+ six colors; the 7th is extended — pair it with a legend). Series index is stable. Use{' '}
297
+ <Text className="font-semibold">default</Text> on neutral/light surfaces (also legible under black
298
+ text) and <Text className="font-semibold">dark</Text> where white text sits on the swatch.
299
+ </Text>
300
+
301
+ <CategoricalRow name="Default" colors={categoricalPalette.default} />
302
+ <CategoricalRow name="Dark (white text)" colors={categoricalPalette.dark} />
303
+ </View>
304
+ ),
305
+ }
306
+
239
307
  export const BorderColors: StoryObj = {
240
308
  render: () => (
241
309
  <View style={{ padding: 24 }}>
@@ -0,0 +1,54 @@
1
+ // Theme surface WITHOUT the ThemeProvider value (dep hygiene, titan 0.5.0).
2
+ //
3
+ // `ThemeProvider` imports `nativewind` (`vars()`). Re-exporting it from the root
4
+ // barrel dragged the whole nativewind import chain into every consumer of
5
+ // `@titan-design/react-ui` — the web SPA paid for a native-only provider. This
6
+ // file is the nativewind-free theme surface: the root barrel re-exports THIS,
7
+ // and the `/theme` subpath (`./index.ts`) layers the `ThemeProvider` value on
8
+ // top for the native consumers that deliberately import it.
9
+ export * from './tokens'
10
+ export * from './config'
11
+ export * from './shadows'
12
+ export * from './elevation'
13
+ export * from './manifest'
14
+ export { resolveColor } from './resolve-color'
15
+ export { linearGradient, surfaceGradient, type GradientStyle } from './gradients'
16
+
17
+ // ThemeProvider TYPES may stay on the root (erased at build — no nativewind
18
+ // pull). Only the ThemeProvider VALUE moves to the `/theme` subpath.
19
+ export type { ThemeProviderProps, ThemeProviderMode } from './ThemeProvider'
20
+
21
+ // Re-export commonly used items
22
+ export { getSemanticColors, type ThemeMode } from './tokens/semantic'
23
+ export { getThemeCSSVars, gluestackConfig } from './config'
24
+ export {
25
+ neumorphicShadows,
26
+ // Color math utilities (HSV-based)
27
+ lighten,
28
+ darken,
29
+ getHoverColors,
30
+ isDark,
31
+ hexToRgb,
32
+ rgbToHsv,
33
+ // Shadow creation functions
34
+ createRaisedShadow,
35
+ createRaisedHoverShadow,
36
+ createPressedShadow,
37
+ createPressedHoverShadow,
38
+ createFlatShadow,
39
+ shadowColors,
40
+ type ShadowIntensity,
41
+ type ShadowSurface,
42
+ } from './shadows'
43
+ export {
44
+ getElevationConfig,
45
+ getElevationSurface,
46
+ getElevationShadow,
47
+ getBaseSurfaceColor,
48
+ getValidatedElevation,
49
+ componentElevationRanges,
50
+ type ElevationLevel,
51
+ type ElevationConfig,
52
+ type ComponentType,
53
+ } from './elevation'
54
+ export * from './presets'
@@ -14,10 +14,11 @@ import { semanticColorsLight, semanticColorsDark, type ThemeMode } from './token
14
14
  const themeIndependentCSSVars = {
15
15
  // RGB decomposed values for glow shadows
16
16
  '--color-brand-primary-rgb': '255, 121, 0',
17
- '--color-brand-secondary-rgb': '64, 109, 135',
18
- '--color-status-success-rgb': '20, 184, 166',
17
+ '--color-brand-secondary-rgb': '48, 123, 155',
18
+ '--color-status-success-rgb': '46, 213, 115',
19
19
  '--color-status-error-rgb': '209, 67, 67',
20
- '--color-status-warning-rgb': '255, 176, 32',
20
+ '--color-status-error-vivid-rgb': '255, 71, 87',
21
+ '--color-status-warning-rgb': '249, 180, 21',
21
22
  '--color-status-info-rgb': '33, 150, 243',
22
23
  '--color-background-base-rgb': '16, 16, 16',
23
24
 
@@ -59,6 +60,11 @@ export const lightThemeCSSVars = {
59
60
  '--color-status-info': semanticColorsLight['status-info'],
60
61
  '--color-status-info-subtle': semanticColorsLight['status-info-subtle'],
61
62
 
63
+ '--color-status-error-vivid': semanticColorsLight['status-error-vivid'],
64
+ '--color-status-error-vivid-light': semanticColorsLight['status-error-vivid-light'],
65
+ '--color-status-error-vivid-dark': semanticColorsLight['status-error-vivid-dark'],
66
+ '--color-status-error-vivid-subtle': semanticColorsLight['status-error-vivid-subtle'],
67
+
62
68
  '--color-text-primary': semanticColorsLight['text-primary'],
63
69
  '--color-text-secondary': semanticColorsLight['text-secondary'],
64
70
  '--color-text-tertiary': semanticColorsLight['text-tertiary'],
@@ -77,6 +83,7 @@ export const lightThemeCSSVars = {
77
83
  '--color-border-default': semanticColorsLight['border-default'],
78
84
  '--color-border-subtle': semanticColorsLight['border-subtle'],
79
85
  '--color-border-strong': semanticColorsLight['border-strong'],
86
+ '--color-border-prominent': semanticColorsLight['border-prominent'],
80
87
  '--color-border-focus': semanticColorsLight['border-focus'],
81
88
 
82
89
  '--color-interactive-hover': semanticColorsLight['interactive-hover'],
@@ -172,6 +179,11 @@ export const darkThemeCSSVars = {
172
179
  '--color-status-info': semanticColorsDark['status-info'],
173
180
  '--color-status-info-subtle': semanticColorsDark['status-info-subtle'],
174
181
 
182
+ '--color-status-error-vivid': semanticColorsDark['status-error-vivid'],
183
+ '--color-status-error-vivid-light': semanticColorsDark['status-error-vivid-light'],
184
+ '--color-status-error-vivid-dark': semanticColorsDark['status-error-vivid-dark'],
185
+ '--color-status-error-vivid-subtle': semanticColorsDark['status-error-vivid-subtle'],
186
+
175
187
  '--color-text-primary': semanticColorsDark['text-primary'],
176
188
  '--color-text-secondary': semanticColorsDark['text-secondary'],
177
189
  '--color-text-tertiary': semanticColorsDark['text-tertiary'],
@@ -190,6 +202,7 @@ export const darkThemeCSSVars = {
190
202
  '--color-border-default': semanticColorsDark['border-default'],
191
203
  '--color-border-subtle': semanticColorsDark['border-subtle'],
192
204
  '--color-border-strong': semanticColorsDark['border-strong'],
205
+ '--color-border-prominent': semanticColorsDark['border-prominent'],
193
206
  '--color-border-focus': semanticColorsDark['border-focus'],
194
207
 
195
208
  '--color-interactive-hover': semanticColorsDark['interactive-hover'],
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Data-viz / status / brand fills, sourced from the color foundations.
3
+ */
4
+
5
+ import { categoricalPalette, primitiveRamps as ramp } from './tokens/primitives'
6
+
7
+ /**
8
+ * Titan categorical fallback palette shared by Treemap and Scatter — the
9
+ * canonical {@link categoricalPalette} (ordered, nested-stable, CVD-safe).
10
+ * Components index into it modulo length, so the 7-color series is sufficient.
11
+ */
12
+ export const DATAVIZ_CATEGORICAL_PALETTE = categoricalPalette.default
13
+
14
+ /**
15
+ * MesoCard / MesoStatusCard 3px top-accent gradient stops (dark → primary →
16
+ * light), on the orange ramp that brand-primary (orange-400) lives in.
17
+ */
18
+ export const MESO_ACCENT_GRADIENT_DARK = ramp.orange[500]
19
+ export const MESO_ACCENT_GRADIENT_LIGHT = ramp.orange[300]
20
+
21
+ export const WORKOUT_PILL_DELOAD = ramp.magenta[600]
@@ -0,0 +1,20 @@
1
+ /**
2
+ * UI-atom fills, sourced from the color foundations.
3
+ */
4
+
5
+ import { categoricalPalette, primitiveRamps as ramp } from './tokens/primitives'
6
+
7
+ /**
8
+ * Spinner `color="primary"` fill — keeps the spinner a cool, on-palette
9
+ * loading accent.
10
+ */
11
+ export const SPINNER_PRIMARY = ramp.cyan[600]
12
+
13
+ export const SPINNER_SECONDARY = ramp.green[400]
14
+
15
+ /**
16
+ * Categorical avatar palette — deterministic per-name fill, the canonical
17
+ * ordered {@link categoricalPalette} (CVD-safe). Order is preserved for stable
18
+ * hashing.
19
+ */
20
+ export const AVATAR_CATEGORICAL_COLORS = categoricalPalette.default
@@ -16,36 +16,42 @@
16
16
  /* Dark mode (default) */
17
17
  :root {
18
18
  --color-brand-primary: #FF7900;
19
- --color-brand-primary-light: #FF9630;
20
- --color-brand-primary-dark: #D0620C;
19
+ --color-brand-primary-light: #FFA063;
20
+ --color-brand-primary-dark: #DA5F00;
21
21
  --color-brand-primary-subtle: rgba(255, 121, 0, 0.12);
22
- --color-brand-secondary: #406D87;
23
- --color-brand-secondary-light: #678498;
24
- --color-brand-secondary-dark: #32556D;
25
- --color-brand-secondary-subtle: rgba(64, 109, 135, 0.12);
26
- --color-brand-primary-hover: #FF8500;
27
- --color-brand-primary-active: #FF9630;
28
- --color-brand-secondary-hover: #557488;
29
- --color-brand-secondary-active: #678498;
22
+ --color-brand-secondary: #307B9B;
23
+ --color-brand-secondary-light: #2697B7;
24
+ --color-brand-secondary-dark: #2A617F;
25
+ --color-brand-secondary-subtle: rgba(48, 123, 155, 0.12);
26
+ --color-brand-primary-hover: #FFA063;
27
+ --color-brand-primary-active: #FFC7A2;
28
+ --color-brand-secondary-hover: #2697B7;
29
+ --color-brand-secondary-active: #01B5D1;
30
30
 
31
31
  --color-on-brand-primary: #FFFFFF;
32
32
  --color-on-brand-secondary: #FFFFFF;
33
33
 
34
- --color-status-success: #14B8A6;
35
- --color-status-success-light: #43C6B7;
36
- --color-status-success-dark: #0E8074;
37
- --color-status-success-subtle: rgba(20, 184, 166, 0.12);
34
+ --color-status-success: #2ED573;
35
+ --color-status-success-light: #58F69E;
36
+ --color-status-success-dark: #298732;
37
+ --color-status-success-subtle: rgba(46, 213, 115, 0.12);
38
38
  --color-status-error: #D14343;
39
- --color-status-error-light: #DA6868;
40
- --color-status-error-dark: #922E2E;
39
+ --color-status-error-light: #E05254;
40
+ --color-status-error-dark: #A4221C;
41
41
  --color-status-error-subtle: rgba(209, 67, 67, 0.12);
42
- --color-status-warning: #FFB020;
43
- --color-status-warning-light: #FFBF4C;
44
- --color-status-warning-dark: #B27B16;
45
- --color-status-warning-subtle: rgba(255, 176, 32, 0.12);
42
+ --color-status-error-vivid: #FF4757;
43
+ --color-status-error-vivid-light: #FF6070;
44
+ --color-status-error-vivid-dark: #C42539;
45
+ --color-status-error-vivid-subtle: rgba(255, 71, 87, 0.12);
46
+ --color-status-error-vivid-rgb: 255, 71, 87;
47
+ --color-border-prominent: #3C3C3C;
48
+ --color-status-warning: #F9B415;
49
+ --color-status-warning-light: #FFD352;
50
+ --color-status-warning-dark: #C27400;
51
+ --color-status-warning-subtle: rgba(249, 180, 21, 0.12);
46
52
  --color-status-info: #2196F3;
47
- --color-status-info-light: #64B6F7;
48
- --color-status-info-dark: #0B79D0;
53
+ --color-status-info-light: #78C2FF;
54
+ --color-status-info-dark: #1072CB;
49
55
  --color-status-info-subtle: rgba(33, 150, 243, 0.12);
50
56
 
51
57
  --color-on-status-success: #FFFFFF;
@@ -119,10 +125,10 @@
119
125
 
120
126
  /* RGB decomposed values for glow shadows */
121
127
  --color-brand-primary-rgb: 255, 121, 0;
122
- --color-brand-secondary-rgb: 64, 109, 135;
123
- --color-status-success-rgb: 20, 184, 166;
128
+ --color-brand-secondary-rgb: 48, 123, 155;
129
+ --color-status-success-rgb: 46, 213, 115;
124
130
  --color-status-error-rgb: 209, 67, 67;
125
- --color-status-warning-rgb: 255, 176, 32;
131
+ --color-status-warning-rgb: 249, 180, 21;
126
132
  --color-status-info-rgb: 33, 150, 243;
127
133
  --color-background-base-rgb: 16, 16, 16;
128
134
 
@@ -145,37 +151,43 @@
145
151
  .light,
146
152
  :root.light {
147
153
  --color-brand-primary: #FF7900;
148
- --color-brand-primary-light: #FF9630;
149
- --color-brand-primary-dark: #D0620C;
154
+ --color-brand-primary-light: #FFA063;
155
+ --color-brand-primary-dark: #DA5F00;
150
156
  --color-brand-primary-subtle: rgba(255, 121, 0, 0.08);
151
- --color-brand-secondary: #406D87;
152
- --color-brand-secondary-light: #678498;
153
- --color-brand-secondary-dark: #32556D;
154
- --color-brand-secondary-subtle: rgba(64, 109, 135, 0.08);
155
- --color-brand-primary-hover: #F56C00;
156
- --color-brand-primary-active: #E06D10;
157
- --color-brand-secondary-hover: #39617A;
158
- --color-brand-secondary-active: #32556D;
157
+ --color-brand-secondary: #307B9B;
158
+ --color-brand-secondary-light: #2697B7;
159
+ --color-brand-secondary-dark: #2A617F;
160
+ --color-brand-secondary-subtle: rgba(48, 123, 155, 0.08);
161
+ --color-brand-primary-hover: #DA5F00;
162
+ --color-brand-primary-active: #B94A00;
163
+ --color-brand-secondary-hover: #2A617F;
164
+ --color-brand-secondary-active: #22465F;
159
165
 
160
166
  --color-on-brand-primary: #FFFFFF;
161
167
  --color-on-brand-secondary: #FFFFFF;
162
168
 
163
- --color-status-success: #14B8A6;
164
- --color-status-success-light: #43C6B7;
165
- --color-status-success-dark: #0E8074;
166
- --color-status-success-subtle: #F0FDFA;
169
+ --color-status-success: #2ED573;
170
+ --color-status-success-light: #58F69E;
171
+ --color-status-success-dark: #298732;
172
+ --color-status-success-subtle: #E3FFEE;
167
173
  --color-status-error: #D14343;
168
- --color-status-error-light: #DA6868;
169
- --color-status-error-dark: #922E2E;
170
- --color-status-error-subtle: #FEF2F2;
171
- --color-status-warning: #FFB020;
172
- --color-status-warning-light: #FFBF4C;
173
- --color-status-warning-dark: #B27B16;
174
- --color-status-warning-subtle: #FFFBEB;
174
+ --color-status-error-light: #E05254;
175
+ --color-status-error-dark: #A4221C;
176
+ --color-status-error-subtle: #FFF4F4;
177
+ --color-status-error-vivid: #FF4757;
178
+ --color-status-error-vivid-light: #FF6070;
179
+ --color-status-error-vivid-dark: #C42539;
180
+ --color-status-error-vivid-subtle: rgba(255, 71, 87, 0.12);
181
+ --color-status-error-vivid-rgb: 255, 71, 87;
182
+ --color-border-prominent: #9CA3AF;
183
+ --color-status-warning: #F9B415;
184
+ --color-status-warning-light: #FFD352;
185
+ --color-status-warning-dark: #C27400;
186
+ --color-status-warning-subtle: #FFF7DD;
175
187
  --color-status-info: #2196F3;
176
- --color-status-info-light: #64B6F7;
177
- --color-status-info-dark: #0B79D0;
178
- --color-status-info-subtle: #F0F9FF;
188
+ --color-status-info-light: #78C2FF;
189
+ --color-status-info-dark: #1072CB;
190
+ --color-status-info-subtle: #EFF8FF;
179
191
 
180
192
  --color-on-status-success: #FFFFFF;
181
193
  --color-on-status-error: #FFFFFF;
@@ -248,10 +260,10 @@
248
260
 
249
261
  /* RGB decomposed values for glow shadows */
250
262
  --color-brand-primary-rgb: 255, 121, 0;
251
- --color-brand-secondary-rgb: 64, 109, 135;
252
- --color-status-success-rgb: 20, 184, 166;
263
+ --color-brand-secondary-rgb: 48, 123, 155;
264
+ --color-status-success-rgb: 46, 213, 115;
253
265
  --color-status-error-rgb: 209, 67, 67;
254
- --color-status-warning-rgb: 255, 176, 32;
266
+ --color-status-warning-rgb: 249, 180, 21;
255
267
  --color-status-info-rgb: 33, 150, 243;
256
268
  --color-background-base-rgb: 16, 16, 16;
257
269
 
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { linearGradient, surfaceGradient } from './gradients'
3
+
4
+ describe('gradients', () => {
5
+ it('builds a linear-gradient backgroundImage from two tokens (web CSS vars)', () => {
6
+ const style = linearGradient('surface-elevated', 'background-base', 180)
7
+ expect(style.backgroundImage).toBe(
8
+ 'linear-gradient(180deg, var(--color-surface-elevated), var(--color-background-base))'
9
+ )
10
+ })
11
+
12
+ it('respects a custom angle', () => {
13
+ expect(linearGradient('surface-base', 'background-base', 90).backgroundImage).toContain('90deg')
14
+ })
15
+
16
+ it('defaults to a 180deg angle', () => {
17
+ expect(linearGradient('surface-base', 'background-base').backgroundImage).toContain('180deg')
18
+ })
19
+
20
+ it('surfaceGradient.chrome is the elevated → base chrome wash', () => {
21
+ expect(surfaceGradient.chrome().backgroundImage).toBe(
22
+ 'linear-gradient(180deg, var(--color-surface-elevated), var(--color-background-base))'
23
+ )
24
+ })
25
+ })
@@ -0,0 +1,36 @@
1
+ import { resolveColor } from './resolve-color'
2
+ import { getSemanticColors, type ThemeMode } from './tokens/semantic'
3
+
4
+ type ColorToken = keyof ReturnType<typeof getSemanticColors>
5
+
6
+ /** A `backgroundImage` style — web-only (RN ignores it); type it loosely for RN style arrays. */
7
+ export type GradientStyle = { backgroundImage: string }
8
+
9
+ /**
10
+ * A linear-gradient `backgroundImage` built from two semantic color tokens.
11
+ *
12
+ * Themeable + native-safe via {@link resolveColor}: on web the stops are CSS
13
+ * variables (light/dark switching keeps working); on native it resolves to hex.
14
+ * `backgroundImage` is a **web-only** paint, so pair this with a solid
15
+ * `bg-*` className fallback for native (see the shell TopBar).
16
+ */
17
+ export function linearGradient(
18
+ from: ColorToken,
19
+ to: ColorToken,
20
+ angle = 180,
21
+ mode: ThemeMode = 'dark'
22
+ ): GradientStyle {
23
+ return {
24
+ backgroundImage: `linear-gradient(${angle}deg, ${resolveColor(from, mode)}, ${resolveColor(to, mode)})`,
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Named surface gradients — the shared substrate for gradient fills. Replaces
30
+ * per-component inline `linear-gradient` strings.
31
+ */
32
+ export const surfaceGradient = {
33
+ /** Chrome bands (top bar, headers): elevated → base, a subtle dark wash. */
34
+ chrome: (mode: ThemeMode = 'dark'): GradientStyle =>
35
+ linearGradient('surface-elevated', 'background-base', 180, mode),
36
+ }
@@ -1,43 +1,8 @@
1
- // Theme exports
2
- export * from './tokens'
3
- export * from './config'
4
- export * from './shadows'
5
- export * from './elevation'
6
- export * from './manifest'
7
- export { ThemeProvider, type ThemeProviderProps, type ThemeProviderMode } from './ThemeProvider'
8
- export { resolveColor } from './resolve-color'
9
-
10
- // Re-export commonly used items
11
- export { getSemanticColors, type ThemeMode } from './tokens/semantic'
12
- export { getThemeCSSVars, gluestackConfig } from './config'
13
- export {
14
- neumorphicShadows,
15
- // Color math utilities (HSV-based)
16
- lighten,
17
- darken,
18
- getHoverColors,
19
- isDark,
20
- hexToRgb,
21
- rgbToHsv,
22
- // Shadow creation functions
23
- createRaisedShadow,
24
- createRaisedHoverShadow,
25
- createPressedShadow,
26
- createPressedHoverShadow,
27
- createFlatShadow,
28
- shadowColors,
29
- type ShadowIntensity,
30
- type ShadowSurface,
31
- } from './shadows'
32
- export {
33
- getElevationConfig,
34
- getElevationSurface,
35
- getElevationShadow,
36
- getBaseSurfaceColor,
37
- getValidatedElevation,
38
- componentElevationRanges,
39
- type ElevationLevel,
40
- type ElevationConfig,
41
- type ComponentType,
42
- } from './elevation'
43
- export * from './presets'
1
+ // Theme subpath entry: @titan-design/react-ui/theme
2
+ //
3
+ // The full theme surface. This is the nativewind-free `./barrel` PLUS the
4
+ // `ThemeProvider` value — which imports `nativewind` and therefore does NOT
5
+ // belong on the root barrel (that split happened in titan 0.5.0). Native
6
+ // consumers wrap their app root in `<ThemeProvider>` and import it from HERE.
7
+ export * from './barrel'
8
+ export { ThemeProvider } from './ThemeProvider'
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { createRequire } from 'node:module'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { darkThemeCSSVars, lightThemeCSSVars } from './config'
6
+ import { getSemanticColors } from './tokens/semantic'
7
+
8
+ /**
9
+ * Native-theming guard (TD-04.11 / VW-20).
10
+ *
11
+ * On native, titan resolves colors two ways, and BOTH can silently render black
12
+ * if a token is undefined:
13
+ * 1. `className` (e.g. `text-text-secondary`) → tailwind config → a
14
+ * `var(--color-*)` custom property that `ThemeProvider` registers at runtime
15
+ * via nativewind `vars(darkThemeCSSVars)`. A tailwind color pointing at a var
16
+ * the theme maps don't define is unset on native → black text/fill.
17
+ * 2. `resolveColor(token)` → `getSemanticColors(mode)[token]` hex on native /
18
+ * `var(--color-${token})` on web. A token missing a hex value, or whose
19
+ * `--color-<token>` var is undefined, renders black (native) or unset (web).
20
+ *
21
+ * This is the exact class of bug that shipped in the 09.33a swap and was only
22
+ * caught on-device. `config.completeness.test.ts` proves the theme maps mirror
23
+ * global.css (the definition side); this guard covers the USAGE side — every
24
+ * color token the design system references must resolve to a defined token in
25
+ * both modes. Static (no render), so it runs in the fast project.
26
+ */
27
+
28
+ const require = createRequire(import.meta.url)
29
+ const uiRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
30
+
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ const tailwindConfig = require(path.join(uiRoot, 'tailwind.config.js')) as any
33
+ const colorTree = tailwindConfig.theme?.extend?.colors ?? {}
34
+
35
+ // Flatten the nested tailwind color tree into `class-suffix -> leaf value`.
36
+ // `DEFAULT` collapses onto its parent (tailwind's `brand.primary.DEFAULT` -> the
37
+ // `brand-primary` class).
38
+ function flattenColors(
39
+ node: Record<string, unknown>,
40
+ prefix: string[] = [],
41
+ ): Record<string, string> {
42
+ const out: Record<string, string> = {}
43
+ for (const [key, value] of Object.entries(node)) {
44
+ const nextPrefix = key === 'DEFAULT' ? prefix : [...prefix, key]
45
+ if (value && typeof value === 'object') {
46
+ Object.assign(out, flattenColors(value as Record<string, unknown>, nextPrefix))
47
+ } else if (typeof value === 'string') {
48
+ out[nextPrefix.join('-')] = value
49
+ }
50
+ }
51
+ return out
52
+ }
53
+
54
+ const colorLeaves = flattenColors(colorTree)
55
+ const definedDark = new Set(Object.keys(darkThemeCSSVars))
56
+ const definedLight = new Set(Object.keys(lightThemeCSSVars))
57
+
58
+ function varReference(value: string): string | null {
59
+ const match = value.match(/var\((--color-[a-z0-9-]+)\)/)
60
+ return match ? match[1] : null
61
+ }
62
+
63
+ // Accept hex, rgb/rgba, and hsl/hsla; reject empty/undefined. (No token should be
64
+ // literally "#000"/"black" in these maps, but that's a design choice, not a
65
+ // resolution bug, so we only assert a *valid, present* color here.)
66
+ const VALID_COLOR = /^(#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\))$/i
67
+
68
+ describe('native theming guard (no color token renders black on native)', () => {
69
+ it('sanity: tailwind colors and theme maps flattened to non-trivial sets', () => {
70
+ expect(Object.keys(colorLeaves).length).toBeGreaterThan(50)
71
+ expect(definedDark.size).toBeGreaterThan(90)
72
+ expect(definedLight.size).toBe(definedDark.size)
73
+ })
74
+
75
+ it('every tailwind color className resolves to a var defined in BOTH native vars maps', () => {
76
+ const undefinedDark: string[] = []
77
+ const undefinedLight: string[] = []
78
+ for (const [suffix, value] of Object.entries(colorLeaves)) {
79
+ const ref = varReference(value)
80
+ if (!ref) continue // literal color (nativewind preset default) — always resolves
81
+ if (!definedDark.has(ref)) undefinedDark.push(`${suffix} -> ${value}`)
82
+ if (!definedLight.has(ref)) undefinedLight.push(`${suffix} -> ${value}`)
83
+ }
84
+ expect(
85
+ undefinedDark,
86
+ 'tailwind colors point at vars missing from darkThemeCSSVars (className renders black on native dark)',
87
+ ).toEqual([])
88
+ expect(
89
+ undefinedLight,
90
+ 'tailwind colors point at vars missing from lightThemeCSSVars (className renders black on native light)',
91
+ ).toEqual([])
92
+ })
93
+
94
+ it('every resolveColor/getSemanticColors token has a valid hex AND a matching --color-<token> var', () => {
95
+ for (const mode of ['dark', 'light'] as const) {
96
+ const colors = getSemanticColors(mode)
97
+ const defined = mode === 'dark' ? definedDark : definedLight
98
+ const invalidValues: string[] = []
99
+ const missingVars: string[] = []
100
+ for (const [token, value] of Object.entries(colors)) {
101
+ if (typeof value !== 'string' || !VALID_COLOR.test(value)) {
102
+ invalidValues.push(`${token} = ${JSON.stringify(value)}`)
103
+ }
104
+ // resolveColor's web path returns `var(--color-${token})`; that var must exist.
105
+ if (!defined.has(`--color-${token}`)) missingVars.push(token)
106
+ }
107
+ expect(
108
+ invalidValues,
109
+ `getSemanticColors('${mode}') has tokens with no valid color (renders black/unset on native)`,
110
+ ).toEqual([])
111
+ expect(
112
+ missingVars,
113
+ `resolveColor tokens missing a --color-<token> in ${mode} vars (renders unset on web)`,
114
+ ).toEqual([])
115
+ }
116
+ })
117
+ })