@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.
- package/dist/MesoStatusCard-CQk71hSi.d.mts +77 -0
- package/dist/MesoStatusCard-CQk71hSi.d.ts +77 -0
- package/dist/bodymap.d.mts +246 -3
- package/dist/bodymap.d.ts +246 -3
- package/dist/bodymap.js +441 -97
- package/dist/bodymap.js.map +1 -1
- package/dist/bodymap.mjs +1958 -66
- package/dist/bodymap.mjs.map +1 -1
- package/dist/index.d.mts +71 -738
- package/dist/index.d.ts +71 -738
- package/dist/index.js +888 -1583
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4515 -2348
- package/dist/index.mjs.map +1 -1
- package/dist/pages-D7Jlssvp.d.ts +791 -0
- package/dist/pages-MO0JCySL.d.mts +791 -0
- package/dist/pages.d.mts +4 -0
- package/dist/pages.d.ts +4 -0
- package/dist/pages.js +6646 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +6626 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/theme/index.d.mts +28 -2
- package/dist/theme/index.d.ts +28 -2
- package/dist/theme/index.js +37 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1880 -2
- package/dist/theme/index.mjs.map +1 -1
- package/dist/theme/tokens-css.mjs +573 -1
- package/dist/theme/tokens-css.mjs.map +1 -1
- package/package.json +9 -7
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +10 -15
- package/src/components/custom/Workout/BaseBadge.stories.tsx +3 -3
- package/src/components/custom/Workout/BaseBadge.test.tsx +1 -1
- package/src/components/custom/Workout/BaseBadge.tsx +1 -1
- package/src/components/custom/Workout/BodyMap.tsx +7 -8
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +25 -28
- package/src/components/custom/Workout/CapacityBandChart.tsx +2 -3
- package/src/components/custom/Workout/ExerciseCard.tsx +17 -28
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +38 -31
- package/src/components/custom/Workout/InputBar.tsx +10 -11
- package/src/components/custom/Workout/IntensityBar.test.tsx +112 -60
- package/src/components/custom/Workout/IntensityBar.tsx +109 -58
- package/src/components/custom/Workout/MesoCard.tsx +2 -6
- package/src/components/custom/Workout/MesoStatusCard.tsx +17 -15
- package/src/components/custom/Workout/MuscleGroupChip.tsx +2 -4
- package/src/components/custom/Workout/PlaceholderStrip.tsx +5 -10
- package/src/components/custom/Workout/PrBadge.tsx +3 -3
- package/src/components/custom/Workout/PrHistoryModal.tsx +14 -16
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -12
- package/src/components/custom/Workout/README.md +50 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +13 -11
- package/src/components/custom/Workout/RestTimer.test.tsx +41 -10
- package/src/components/custom/Workout/RestTimer.tsx +55 -49
- package/src/components/custom/Workout/SetRow.tsx +20 -8
- package/src/components/custom/Workout/StrengthTrendChart.tsx +15 -17
- package/src/components/custom/Workout/TempoBar.stories.tsx +95 -0
- package/src/components/custom/Workout/TempoBar.test.tsx +103 -0
- package/src/components/custom/Workout/TempoBar.tsx +215 -0
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +69 -2
- package/src/components/custom/Workout/TempoDisplay.test.tsx +35 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +104 -1
- package/src/components/custom/Workout/TrainingStatusPage.tsx +8 -17
- package/src/components/custom/Workout/VelocityStrip.test.tsx +89 -2
- package/src/components/custom/Workout/VelocityStrip.tsx +210 -43
- package/src/components/custom/Workout/WeekRow.test.tsx +2 -2
- package/src/components/custom/Workout/WeekRow.tsx +2 -1
- package/src/components/custom/Workout/WeightBadge.test.tsx +2 -2
- package/src/components/custom/Workout/WeightBadge.tsx +2 -2
- package/src/components/custom/Workout/WorkoutCard.tsx +3 -8
- package/src/components/custom/Workout/icons.tsx +68 -0
- package/src/components/custom/Workout/index.ts +47 -47
- package/src/index.ts +3 -2
- package/src/pages.ts +61 -0
- package/src/test/nativewind-stub.ts +13 -0
- package/src/test/stories-smoke.test.tsx +50 -0
- package/src/theme/ThemeProvider.test.tsx +24 -0
- package/src/theme/ThemeProvider.tsx +50 -0
- package/src/theme/barrel.ts +53 -0
- package/src/theme/index.ts +8 -41
- package/src/theme/native-theming-guard.test.ts +117 -0
- package/src/theme/resolve-color.ts +19 -0
- package/dist/bodymap-BhG55xHM.d.mts +0 -318
- package/dist/bodymap-BhG55xHM.d.ts +0 -318
- package/dist/chunk-7XPB4NAO.mjs +0 -1013
- package/dist/chunk-7XPB4NAO.mjs.map +0 -1
- package/dist/chunk-P7TSQUN6.mjs +0 -846
- package/dist/chunk-P7TSQUN6.mjs.map +0 -1
- package/dist/chunk-TOD2WQBG.mjs +0 -1048
- 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 {
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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'
|
package/src/theme/index.ts
CHANGED
|
@@ -1,41 +1,8 @@
|
|
|
1
|
-
// Theme
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}
|