@titan-design/react-ui 0.3.0 → 0.5.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 (90) 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 +441 -97
  6. package/dist/bodymap.js.map +1 -1
  7. package/dist/bodymap.mjs +1958 -66
  8. package/dist/bodymap.mjs.map +1 -1
  9. package/dist/index.d.mts +71 -738
  10. package/dist/index.d.ts +71 -738
  11. package/dist/index.js +888 -1583
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +4515 -2348
  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 +6646 -0
  20. package/dist/pages.js.map +1 -0
  21. package/dist/pages.mjs +6626 -0
  22. package/dist/pages.mjs.map +1 -0
  23. package/dist/theme/index.d.mts +28 -2
  24. package/dist/theme/index.d.ts +28 -2
  25. package/dist/theme/index.js +37 -0
  26. package/dist/theme/index.js.map +1 -1
  27. package/dist/theme/index.mjs +1880 -2
  28. package/dist/theme/index.mjs.map +1 -1
  29. package/dist/theme/tokens-css.mjs +573 -1
  30. package/dist/theme/tokens-css.mjs.map +1 -1
  31. package/package.json +9 -7
  32. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +10 -15
  33. package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
  34. package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
  35. package/src/components/custom/Workout/BaseBadge.tsx +1 -1
  36. package/src/components/custom/Workout/BodyMap.tsx +7 -8
  37. package/src/components/custom/Workout/BodyMapDetailPanel.tsx +25 -28
  38. package/src/components/custom/Workout/CapacityBandChart.tsx +2 -3
  39. package/src/components/custom/Workout/ExerciseCard.tsx +17 -28
  40. package/src/components/custom/Workout/ExerciseDetailPage.tsx +38 -31
  41. package/src/components/custom/Workout/InputBar.tsx +10 -11
  42. package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
  43. package/src/components/custom/Workout/IntensityBar.tsx +109 -58
  44. package/src/components/custom/Workout/MesoCard.tsx +2 -6
  45. package/src/components/custom/Workout/MesoStatusCard.tsx +17 -15
  46. package/src/components/custom/Workout/MuscleGroupChip.tsx +2 -4
  47. package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -10
  48. package/src/components/custom/Workout/PrBadge.tsx +3 -3
  49. package/src/components/custom/Workout/PrHistoryModal.tsx +14 -16
  50. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -12
  51. package/src/components/custom/Workout/README.md +50 -0
  52. package/src/components/custom/Workout/ReadinessCheck.tsx +13 -11
  53. package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
  54. package/src/components/custom/Workout/RestTimer.tsx +55 -49
  55. package/src/components/custom/Workout/SetRow.tsx +20 -8
  56. package/src/components/custom/Workout/StrengthTrendChart.tsx +15 -17
  57. package/src/components/custom/Workout/TempoBar.stories.tsx +95 -0
  58. package/src/components/custom/Workout/TempoBar.test.tsx +103 -0
  59. package/src/components/custom/Workout/TempoBar.tsx +215 -0
  60. package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
  61. package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
  62. package/src/components/custom/Workout/TempoDisplay.tsx +104 -1
  63. package/src/components/custom/Workout/TrainingStatusPage.tsx +8 -17
  64. package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
  65. package/src/components/custom/Workout/VelocityStrip.tsx +210 -43
  66. package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
  67. package/src/components/custom/Workout/WeekRow.tsx +2 -1
  68. package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
  69. package/src/components/custom/Workout/WeightBadge.tsx +2 -2
  70. package/src/components/custom/Workout/WorkoutCard.tsx +3 -8
  71. package/src/components/custom/Workout/icons.tsx +68 -0
  72. package/src/components/custom/Workout/index.ts +47 -47
  73. package/src/index.ts +3 -2
  74. package/src/pages.ts +61 -0
  75. package/src/test/nativewind-stub.ts +13 -0
  76. package/src/test/stories-smoke.test.tsx +50 -0
  77. package/src/theme/ThemeProvider.test.tsx +24 -0
  78. package/src/theme/ThemeProvider.tsx +50 -0
  79. package/src/theme/barrel.ts +53 -0
  80. package/src/theme/index.ts +8 -41
  81. package/src/theme/native-theming-guard.test.ts +117 -0
  82. package/src/theme/resolve-color.ts +19 -0
  83. package/dist/bodymap-BhG55xHM.d.mts +0 -318
  84. package/dist/bodymap-BhG55xHM.d.ts +0 -318
  85. package/dist/chunk-7XPB4NAO.mjs +0 -1013
  86. package/dist/chunk-7XPB4NAO.mjs.map +0 -1
  87. package/dist/chunk-P7TSQUN6.mjs +0 -846
  88. package/dist/chunk-P7TSQUN6.mjs.map +0 -1
  89. package/dist/chunk-TOD2WQBG.mjs +0 -1048
  90. package/dist/chunk-TOD2WQBG.mjs.map +0 -1
@@ -8,13 +8,28 @@ export { WeightBadge, type WeightBadgeProps, type WeightBadgeSize } from './Weig
8
8
  export { PrBadge, type PrBadgeProps, type PRType } from './PrBadge'
9
9
  export { StatusDot, type StatusDotVariant, type StatusDotProps } from './StatusDot'
10
10
  export { PlaceholderStrip, type PlaceholderStripProps } from './PlaceholderStrip'
11
- export { TempoDisplay, type TempoDisplayProps } from './TempoDisplay'
11
+ export {
12
+ TempoDisplay,
13
+ type TempoDisplayProps,
14
+ type TempoLivePhase,
15
+ type TempoLiveState,
16
+ } from './TempoDisplay'
17
+ export {
18
+ TempoBar,
19
+ type TempoBarProps,
20
+ type TempoPhaseKey,
21
+ type TempoPacingState,
22
+ TEMPO_PACING,
23
+ getTempoPacingState,
24
+ getTempoFillPct,
25
+ } from './TempoBar'
12
26
  export { DeviationBar, type DeviationBarProps } from './DeviationBar'
13
27
  export { IntensityBar, type IntensityBarProps } from './IntensityBar'
14
28
  export { WorkoutPill, type WorkoutPillProps, type WorkoutPillStatus } from './WorkoutPill'
15
29
  export {
16
30
  VelocityStrip,
17
31
  type VelocityStripProps,
32
+ type VelocityZoneBandProp,
18
33
  getVelocityZoneColor,
19
34
  getVelocityZoneName,
20
35
  calculateVelocityLoss,
@@ -94,37 +109,29 @@ export type {
94
109
  TrainingStatusMuscle,
95
110
  TrainingStatusSummary,
96
111
  } from './TrainingStatusPage'
97
- export {
98
- ProgramPlanningPage,
99
- deriveNavLevel,
100
- toProgressBarMesos,
101
- findMeso,
102
- findWeek,
103
- findWorkout,
104
- buildBreadcrumbs,
105
- type ProgramPlanningPageProps,
106
- type PlanMeso,
107
- type PlanWeek,
108
- type PlanWorkout,
109
- type ProgramNavLevel,
110
- type ProgramSelection,
111
- type ProgramBreadcrumb,
112
+ // ActiveWorkoutPage / ExerciseDetailPage / ProgramPlanningPage are page-level
113
+ // organisms demoted to the `@titan-design/react-ui/pages` subpath (titan 0.5.0).
114
+ // Their VALUE exports (components + derivation helpers) live there; only
115
+ // type-only re-exports stay on this barrel (erased at build, no runtime pull).
116
+ export type {
117
+ ProgramPlanningPageProps,
118
+ PlanMeso,
119
+ PlanWeek,
120
+ PlanWorkout,
121
+ ProgramNavLevel,
122
+ ProgramSelection,
123
+ ProgramBreadcrumb,
112
124
  } from './ProgramPlanningPage'
113
- export {
114
- ExerciseDetailPage,
115
- deriveExerciseStats,
116
- toExerciseCardProps,
117
- summarizeVbt,
118
- EXERCISE_DETAIL_TABS,
119
- type ExerciseDetailPageProps,
120
- type ExerciseDetailTab,
121
- type ExerciseDetailHeader,
122
- type ExerciseDetailEntry,
123
- type ExerciseTrend,
124
- type ExerciseVbtSet,
125
- type ExerciseVbt,
126
- type ExerciseDetailStats,
127
- type VbtSummary,
125
+ export type {
126
+ ExerciseDetailPageProps,
127
+ ExerciseDetailTab,
128
+ ExerciseDetailHeader,
129
+ ExerciseDetailEntry,
130
+ ExerciseTrend,
131
+ ExerciseVbtSet,
132
+ ExerciseVbt,
133
+ ExerciseDetailStats,
134
+ VbtSummary,
128
135
  } from './ExerciseDetailPage'
129
136
  // muscleTaxonomy imports `react-native-body-highlighter` at runtime. Its value
130
137
  // exports (incl. the MuscleGroup / SimpleMuscleGroup enums) live behind the
@@ -135,20 +142,13 @@ export type {
135
142
  MovementCategory,
136
143
  VolumeLandmarks,
137
144
  } from './muscleTaxonomy'
138
- export {
139
- ActiveWorkoutPage,
140
- countCompletedSets,
141
- deriveWorkoutProgress,
142
- findActiveExercise,
143
- statusToCardState,
144
- toActiveCardProps,
145
- groupExercises,
146
- type ActiveWorkoutPageProps,
147
- type ActiveWorkoutExercise,
148
- type ActiveWorkoutSuperset,
149
- type ActiveWorkoutInput,
150
- type ActiveWorkoutRest,
151
- type ActiveExerciseStatus,
152
- type WorkoutProgress,
153
- type WorkoutGroup,
145
+ export type {
146
+ ActiveWorkoutPageProps,
147
+ ActiveWorkoutExercise,
148
+ ActiveWorkoutSuperset,
149
+ ActiveWorkoutInput,
150
+ ActiveWorkoutRest,
151
+ ActiveExerciseStatus,
152
+ WorkoutProgress,
153
+ WorkoutGroup,
154
154
  } from './ActiveWorkoutPage'
package/src/index.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  // Components
4
4
  export * from './components'
5
5
 
6
- // Theme
7
- export * from './theme'
6
+ // Theme (nativewind-free surface; ThemeProvider value lives on the /theme
7
+ // subpath so the root barrel does not pull the nativewind import chain)
8
+ export * from './theme/barrel'
8
9
 
9
10
  // Utilities
10
11
  export * from './utils'
package/src/pages.ts ADDED
@@ -0,0 +1,61 @@
1
+ // Subpath entry: @titan-design/react-ui/pages
2
+ //
3
+ // Page-level organisms — phone-shaped reference screens (ActiveWorkoutPage,
4
+ // ExerciseDetailPage, ProgramPlanningPage). Demoted out of the root barrel for
5
+ // titan 0.5.0: they are full app screens, orphaned by both consumers, and their
6
+ // successors are the form-factor-responsive level views (plan B2). Their VALUE
7
+ // exports (components + derivation helpers) live HERE; type-only re-exports
8
+ // stay on the root barrel so consumers can still type props without importing
9
+ // the screens. Mirrors the `/bodymap` isolation pattern.
10
+ //
11
+ // TrainingStatusPage is NOT here — it depends on react-native-body-highlighter
12
+ // and lives under `@titan-design/react-ui/bodymap`.
13
+ export {
14
+ ActiveWorkoutPage,
15
+ countCompletedSets,
16
+ deriveWorkoutProgress,
17
+ findActiveExercise,
18
+ statusToCardState,
19
+ toActiveCardProps,
20
+ groupExercises,
21
+ type ActiveWorkoutPageProps,
22
+ type ActiveWorkoutExercise,
23
+ type ActiveWorkoutSuperset,
24
+ type ActiveWorkoutInput,
25
+ type ActiveWorkoutRest,
26
+ type ActiveExerciseStatus,
27
+ type WorkoutProgress,
28
+ type WorkoutGroup,
29
+ } from './components/custom/Workout/ActiveWorkoutPage'
30
+ export {
31
+ ExerciseDetailPage,
32
+ deriveExerciseStats,
33
+ toExerciseCardProps,
34
+ summarizeVbt,
35
+ EXERCISE_DETAIL_TABS,
36
+ type ExerciseDetailPageProps,
37
+ type ExerciseDetailTab,
38
+ type ExerciseDetailHeader,
39
+ type ExerciseDetailEntry,
40
+ type ExerciseTrend,
41
+ type ExerciseVbtSet,
42
+ type ExerciseVbt,
43
+ type ExerciseDetailStats,
44
+ type VbtSummary,
45
+ } from './components/custom/Workout/ExerciseDetailPage'
46
+ export {
47
+ ProgramPlanningPage,
48
+ deriveNavLevel,
49
+ toProgressBarMesos,
50
+ findMeso,
51
+ findWeek,
52
+ findWorkout,
53
+ buildBreadcrumbs,
54
+ type ProgramPlanningPageProps,
55
+ type PlanMeso,
56
+ type PlanWeek,
57
+ type PlanWorkout,
58
+ type ProgramNavLevel,
59
+ type ProgramSelection,
60
+ type ProgramBreadcrumb,
61
+ } from './components/custom/Workout/ProgramPlanningPage'
@@ -0,0 +1,13 @@
1
+ // Web test stub for `nativewind`.
2
+ //
3
+ // The real `nativewind` pulls react-native-css-interop → RN's Flow-typed
4
+ // sources, which the jsdom/react-native-web test environment can't parse
5
+ // ("Unexpected token 'typeof'"). Only `ThemeProvider` imports nativewind (for
6
+ // `vars()`), and only for its NATIVE behavior — on web the color tokens resolve
7
+ // from `global.css`, so an identity `vars()` is a faithful web stand-in.
8
+ // Aliased in `vitest.config.ts`; the real package is used at build/native.
9
+
10
+ /** Identity stand-in for nativewind's `vars()` — returns the token map as-is. */
11
+ export function vars(values: Record<string, string>): Record<string, string> {
12
+ return values
13
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render } from '@testing-library/react'
3
+ import { composeStories } from '@storybook/react'
4
+
5
+ /**
6
+ * Storybook → render-test bridge (TD-04.11 / VW-20).
7
+ *
8
+ * The design system has a complete `*.stories.tsx` set but no test consumed it.
9
+ * This uses Storybook's portable-stories `composeStories` to mount EVERY story
10
+ * (with its args/decorators applied) through the same react-native-web + jsdom
11
+ * pipeline the unit tests use, asserting each renders without throwing and emits
12
+ * output. It's broad, near-free coverage that catches "this organism/variant
13
+ * crashes on render" regressions across the whole component set — the long tail
14
+ * the hand-written per-organism tests don't enumerate.
15
+ *
16
+ * Project annotations (the preview's theme decorator) are intentionally NOT
17
+ * applied: a smoke test only needs to prove each story renders, and skipping
18
+ * them avoids importing global.css / addon-themes into the test env.
19
+ */
20
+
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ type StoryModule = Record<string, any>
23
+
24
+ const storyModules = import.meta.glob<StoryModule>('../components/**/*.stories.tsx', {
25
+ eager: true,
26
+ })
27
+
28
+ describe('storybook stories render (composeStories smoke)', () => {
29
+ const entries = Object.entries(storyModules)
30
+
31
+ it('discovers a non-trivial number of story modules', () => {
32
+ expect(entries.length).toBeGreaterThan(10)
33
+ })
34
+
35
+ for (const [file, mod] of entries) {
36
+ const label = file.replace('../components/', '')
37
+ const composed = composeStories(mod)
38
+ for (const [name, Story] of Object.entries(composed)) {
39
+ it(`${label} › ${name} renders`, () => {
40
+ // Smoke level: every story must MOUNT without throwing. `firstChild` is
41
+ // not asserted — portal/modal organisms (PrHistoryModal) render into
42
+ // document.body, and empty-state stories legitimately render null; the
43
+ // hand-written per-organism tests own the detailed output assertions.
44
+ const result = render(<Story />)
45
+ expect(result.unmount).toBeTypeOf('function')
46
+ result.unmount()
47
+ })
48
+ }
49
+ }
50
+ })
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { Text } from 'react-native'
4
+ import { ThemeProvider } from './ThemeProvider'
5
+
6
+ describe('ThemeProvider', () => {
7
+ it('renders its children', () => {
8
+ render(
9
+ <ThemeProvider>
10
+ <Text>inside</Text>
11
+ </ThemeProvider>,
12
+ )
13
+ expect(screen.getByText('inside')).toBeInTheDocument()
14
+ })
15
+
16
+ it('accepts a light mode without error', () => {
17
+ render(
18
+ <ThemeProvider mode="light">
19
+ <Text>light</Text>
20
+ </ThemeProvider>,
21
+ )
22
+ expect(screen.getByText('light')).toBeInTheDocument()
23
+ })
24
+ })
@@ -0,0 +1,50 @@
1
+ // Cross-platform theme provider (TD native-theming).
2
+ //
3
+ // Registers titan's semantic color tokens as runtime CSS variables via
4
+ // nativewind's `vars()` so that className color tokens (`text-text-secondary`,
5
+ // `bg-surface-raised`, `border-border`, …) resolve on NATIVE React Native — the
6
+ // native equivalent of the `:root {}` block `global.css` provides on web.
7
+ //
8
+ // Why this exists: titan's Workout organisms reference semantic colors as CSS
9
+ // variables. On web (the dashboard, react-native-web) those come from
10
+ // `global.css`. On native (the mobile app) nothing defined them, so any titan
11
+ // component using a `--color-*` token rendered with an unresolved (black) color.
12
+ // Wrap the mobile app root in `<ThemeProvider>` and the tokens resolve on-device.
13
+ //
14
+ // The var maps are the SAME canonical `darkThemeCSSVars` / `lightThemeCSSVars`
15
+ // that generate `global.css`, so native and web resolve to identical values.
16
+ import { vars } from 'nativewind'
17
+ import { View, type ViewProps } from 'react-native'
18
+ import { darkThemeCSSVars, lightThemeCSSVars } from './config'
19
+
20
+ export type ThemeProviderMode = 'dark' | 'light'
21
+
22
+ // Precomputed once — `vars()` is pure, so the same style object is reused across
23
+ // renders. Native == web because these are the maps `tokens-css` emits.
24
+ const MODE_VARS = {
25
+ dark: vars(darkThemeCSSVars),
26
+ light: vars(lightThemeCSSVars),
27
+ }
28
+
29
+ export interface ThemeProviderProps extends ViewProps {
30
+ /** Theme mode to register. Mobile is dark-only today; defaults to `dark`. */
31
+ mode?: ThemeProviderMode
32
+ }
33
+
34
+ /**
35
+ * Wrap the app root so descendant titan components' `--color-*` className tokens
36
+ * resolve on native. Fills its parent (`flex: 1`) by default; pass `style` to
37
+ * override. On web it is harmless (the dashboard already loads `global.css`).
38
+ */
39
+ export function ThemeProvider({
40
+ mode = 'dark',
41
+ style,
42
+ children,
43
+ ...props
44
+ }: ThemeProviderProps): React.JSX.Element {
45
+ return (
46
+ <View style={[MODE_VARS[mode], { flex: 1 }, style]} {...props}>
47
+ {children}
48
+ </View>
49
+ )
50
+ }
@@ -0,0 +1,53 @@
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
+
16
+ // ThemeProvider TYPES may stay on the root (erased at build — no nativewind
17
+ // pull). Only the ThemeProvider VALUE moves to the `/theme` subpath.
18
+ export type { ThemeProviderProps, ThemeProviderMode } from './ThemeProvider'
19
+
20
+ // Re-export commonly used items
21
+ export { getSemanticColors, type ThemeMode } from './tokens/semantic'
22
+ export { getThemeCSSVars, gluestackConfig } from './config'
23
+ export {
24
+ neumorphicShadows,
25
+ // Color math utilities (HSV-based)
26
+ lighten,
27
+ darken,
28
+ getHoverColors,
29
+ isDark,
30
+ hexToRgb,
31
+ rgbToHsv,
32
+ // Shadow creation functions
33
+ createRaisedShadow,
34
+ createRaisedHoverShadow,
35
+ createPressedShadow,
36
+ createPressedHoverShadow,
37
+ createFlatShadow,
38
+ shadowColors,
39
+ type ShadowIntensity,
40
+ type ShadowSurface,
41
+ } from './shadows'
42
+ export {
43
+ getElevationConfig,
44
+ getElevationSurface,
45
+ getElevationShadow,
46
+ getBaseSurfaceColor,
47
+ getValidatedElevation,
48
+ componentElevationRanges,
49
+ type ElevationLevel,
50
+ type ElevationConfig,
51
+ type ComponentType,
52
+ } from './elevation'
53
+ export * from './presets'
@@ -1,41 +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
-
8
- // Re-export commonly used items
9
- export { getSemanticColors, type ThemeMode } from './tokens/semantic'
10
- export { getThemeCSSVars, gluestackConfig } from './config'
11
- export {
12
- neumorphicShadows,
13
- // Color math utilities (HSV-based)
14
- lighten,
15
- darken,
16
- getHoverColors,
17
- isDark,
18
- hexToRgb,
19
- rgbToHsv,
20
- // Shadow creation functions
21
- createRaisedShadow,
22
- createRaisedHoverShadow,
23
- createPressedShadow,
24
- createPressedHoverShadow,
25
- createFlatShadow,
26
- shadowColors,
27
- type ShadowIntensity,
28
- type ShadowSurface,
29
- } from './shadows'
30
- export {
31
- getElevationConfig,
32
- getElevationSurface,
33
- getElevationShadow,
34
- getBaseSurfaceColor,
35
- getValidatedElevation,
36
- componentElevationRanges,
37
- type ElevationLevel,
38
- type ElevationConfig,
39
- type ComponentType,
40
- } from './elevation'
41
- 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
+ })
@@ -0,0 +1,19 @@
1
+ import { Platform } from 'react-native'
2
+ import { getSemanticColors, type ThemeMode } from './tokens/semantic'
3
+
4
+ type ColorToken = keyof ReturnType<typeof getSemanticColors>
5
+
6
+ /**
7
+ * Resolve a semantic color token for an inline style where a nativewind
8
+ * className can't apply — gradients (`backgroundImage`), per-edge borders
9
+ * (`borderTopColor`, …), SVG attributes, and conditional hex/token mixes.
10
+ *
11
+ * The companion to className color tokens: on **web** it returns the CSS
12
+ * variable (`var(--color-…)`), so the dashboard's light/dark switching keeps
13
+ * working exactly as `global.css` intends; on **native** it returns the
14
+ * resolved hex from `getSemanticColors` (mobile is dark-only). This is why
15
+ * these spots stay theme-correct on web while also rendering on-device.
16
+ */
17
+ export function resolveColor(token: ColorToken, mode: ThemeMode = 'dark'): string {
18
+ return Platform.OS === 'web' ? `var(--color-${token})` : getSemanticColors(mode)[token]
19
+ }