@titan-design/react-ui 0.2.6 → 0.2.7
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/chunk-3VLOBS6O.mjs +846 -0
- package/dist/chunk-3VLOBS6O.mjs.map +1 -0
- package/dist/{chunk-UXVRPOME.mjs → chunk-5SSKGS6E.mjs} +3 -783
- package/dist/chunk-5SSKGS6E.mjs.map +1 -0
- package/dist/index.d.mts +1005 -4
- package/dist/index.d.ts +1005 -4
- package/dist/index.js +6043 -213
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5901 -177
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +57 -1
- package/dist/theme/index.d.ts +57 -1
- package/dist/theme/index.js +63 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -1
- package/dist/theme/tokens-css.d.mts +18 -0
- package/dist/theme/tokens-css.d.ts +18 -0
- package/dist/theme/tokens-css.js +289 -0
- package/dist/theme/tokens-css.js.map +1 -0
- package/dist/theme/tokens-css.mjs +24 -0
- package/dist/theme/tokens-css.mjs.map +1 -0
- package/package.json +16 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
- package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
- package/src/components/custom/Gauge/Gauge.tsx +167 -0
- package/src/components/custom/Gauge/index.ts +2 -0
- package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
- package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
- package/src/components/custom/Scatter/Scatter.tsx +260 -0
- package/src/components/custom/Scatter/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
- package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
- package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
- package/src/components/custom/Treemap/Treemap.tsx +192 -0
- package/src/components/custom/Treemap/index.ts +2 -0
- package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
- package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
- package/src/components/custom/Workout/BaseBadge.tsx +113 -0
- package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
- package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
- package/src/components/custom/Workout/BodyMap.tsx +348 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +478 -0
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
- package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
- package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
- package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
- package/src/components/custom/Workout/DeviationBar.tsx +78 -0
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +319 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +429 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
- package/src/components/custom/Workout/InputBar.test.tsx +116 -0
- package/src/components/custom/Workout/InputBar.tsx +163 -0
- package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
- package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
- package/src/components/custom/Workout/IntensityBar.tsx +166 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
- package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
- package/src/components/custom/Workout/MesoCard.tsx +235 -0
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +141 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +161 -0
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
- package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
- package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
- package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
- package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
- package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
- package/src/components/custom/Workout/PlaceholderStrip.tsx +91 -0
- package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
- package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
- package/src/components/custom/Workout/PrBadge.tsx +103 -0
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
- package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
- package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +266 -0
- package/src/components/custom/Workout/RestTimer.tsx +8 -6
- package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
- package/src/components/custom/Workout/SetRow.test.tsx +222 -0
- package/src/components/custom/Workout/SetRow.tsx +253 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
- package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
- package/src/components/custom/Workout/Sparkline.tsx +188 -0
- package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
- package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
- package/src/components/custom/Workout/StatusDot.tsx +159 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
- package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
- package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +207 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +234 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +287 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
- package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
- package/src/components/custom/Workout/WeekRow.tsx +140 -0
- package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
- package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
- package/src/components/custom/Workout/WeightBadge.tsx +113 -0
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
- package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
- package/src/components/custom/Workout/WorkoutCard.tsx +226 -0
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
- package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
- package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
- package/src/components/custom/Workout/index.ts +113 -0
- package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
- package/src/components/custom/index.ts +3 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +2 -2
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/card/Card.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +4 -4
- package/src/components/ui/select/Select.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +4 -4
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
- package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
- package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
- package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
- package/src/stories/PresetShowcase.stories.tsx +15 -15
- package/src/theme/index.ts +2 -1
- package/src/theme/manifest/css-property-manifest.example.json +79 -0
- package/src/theme/manifest/css-property-manifest.schema.json +93 -0
- package/src/theme/manifest/css-property-manifest.test.ts +81 -0
- package/src/theme/manifest/css-property-manifest.types.ts +49 -0
- package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
- package/src/theme/manifest/index.ts +11 -0
- package/src/theme/tokens-css.test.ts +28 -0
- package/src/theme/tokens-css.ts +34 -0
- package/src/theme/workout-tokens.ts +61 -0
- package/dist/chunk-UXVRPOME.mjs.map +0 -1
|
@@ -319,7 +319,7 @@ export const CompactMode: Story = {
|
|
|
319
319
|
onPress={() => setIsCompact(!isCompact)}
|
|
320
320
|
/>
|
|
321
321
|
</View>
|
|
322
|
-
<View className="h-px bg-border
|
|
322
|
+
<View className="h-px bg-border" />
|
|
323
323
|
<ToolbarButtonGroup gap="sm">
|
|
324
324
|
<ToolbarButton
|
|
325
325
|
label="Filter"
|
|
@@ -231,7 +231,7 @@ export function ToolbarButton({
|
|
|
231
231
|
<View
|
|
232
232
|
className={cn(
|
|
233
233
|
'absolute z-50 top-full left-0 mt-1',
|
|
234
|
-
'rounded-lg shadow-lg border border-border
|
|
234
|
+
'rounded-lg shadow-lg border border-border',
|
|
235
235
|
'min-w-[150px] overflow-hidden',
|
|
236
236
|
`bg-[${MENU_BG}]`
|
|
237
237
|
)}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { View, Text } from 'react-native'
|
|
2
|
+
import {
|
|
3
|
+
Controller,
|
|
4
|
+
type Control,
|
|
5
|
+
type FieldPath,
|
|
6
|
+
type FieldValues,
|
|
7
|
+
type RegisterOptions,
|
|
8
|
+
} from 'react-hook-form'
|
|
9
|
+
import { FormField } from '../../components/ui/form-field'
|
|
10
|
+
import { Input } from '../../components/ui/input'
|
|
11
|
+
import { Select, type SelectOption } from '../../components/ui/select'
|
|
12
|
+
import { Checkbox } from '../../components/ui/checkbox'
|
|
13
|
+
import { RadioGroup, Radio } from '../../components/ui/radio'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reusable react-hook-form adapters for the styled form primitives.
|
|
17
|
+
*
|
|
18
|
+
* Each adapter follows the canonical RHF `Controller` render-prop idiom: the
|
|
19
|
+
* library owns the value/validation state and hands it to the already-styled
|
|
20
|
+
* controlled component through `field` + `fieldState`. None of the components'
|
|
21
|
+
* public APIs are touched — this is pure wiring a consumer would write.
|
|
22
|
+
*/
|
|
23
|
+
interface BaseControlledProps<T extends FieldValues> {
|
|
24
|
+
/** The `control` object returned by `useForm`. */
|
|
25
|
+
control: Control<T>
|
|
26
|
+
/** The field name — type-checked against the form's value shape. */
|
|
27
|
+
name: FieldPath<T>
|
|
28
|
+
/** RHF validation rules (required, pattern, validate, ...). */
|
|
29
|
+
rules?: Omit<
|
|
30
|
+
RegisterOptions<T, FieldPath<T>>,
|
|
31
|
+
'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
|
|
32
|
+
>
|
|
33
|
+
/** Visible field label. */
|
|
34
|
+
label?: string
|
|
35
|
+
/** Marks the field as required in the UI. */
|
|
36
|
+
isRequired?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ControlledInputProps<T extends FieldValues> extends BaseControlledProps<T> {
|
|
40
|
+
placeholder?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `FormField` + `Input` driven by RHF. Surfaces validation via `errorMessage`. */
|
|
44
|
+
export function ControlledInput<T extends FieldValues>({
|
|
45
|
+
control,
|
|
46
|
+
name,
|
|
47
|
+
rules,
|
|
48
|
+
label,
|
|
49
|
+
isRequired,
|
|
50
|
+
placeholder,
|
|
51
|
+
}: ControlledInputProps<T>) {
|
|
52
|
+
return (
|
|
53
|
+
<Controller
|
|
54
|
+
control={control}
|
|
55
|
+
name={name}
|
|
56
|
+
rules={rules}
|
|
57
|
+
render={({ field, fieldState }) => (
|
|
58
|
+
<FormField label={label} isRequired={isRequired} errorMessage={fieldState.error?.message}>
|
|
59
|
+
<Input
|
|
60
|
+
value={(field.value as string | undefined) ?? ''}
|
|
61
|
+
onChangeText={field.onChange}
|
|
62
|
+
onBlur={field.onBlur}
|
|
63
|
+
placeholder={placeholder}
|
|
64
|
+
/>
|
|
65
|
+
</FormField>
|
|
66
|
+
)}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ControlledSelectProps<T extends FieldValues> extends BaseControlledProps<T> {
|
|
72
|
+
options: SelectOption[]
|
|
73
|
+
placeholder?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `FormField` + `Select` (single-select) driven by RHF. */
|
|
77
|
+
export function ControlledSelect<T extends FieldValues>({
|
|
78
|
+
control,
|
|
79
|
+
name,
|
|
80
|
+
rules,
|
|
81
|
+
label,
|
|
82
|
+
isRequired,
|
|
83
|
+
options,
|
|
84
|
+
placeholder,
|
|
85
|
+
}: ControlledSelectProps<T>) {
|
|
86
|
+
return (
|
|
87
|
+
<Controller
|
|
88
|
+
control={control}
|
|
89
|
+
name={name}
|
|
90
|
+
rules={rules}
|
|
91
|
+
render={({ field, fieldState }) => (
|
|
92
|
+
<FormField label={label} isRequired={isRequired} errorMessage={fieldState.error?.message}>
|
|
93
|
+
<Select
|
|
94
|
+
value={(field.value as string | null | undefined) ?? null}
|
|
95
|
+
onChange={field.onChange}
|
|
96
|
+
options={options}
|
|
97
|
+
placeholder={placeholder}
|
|
98
|
+
/>
|
|
99
|
+
</FormField>
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `Checkbox` (boolean) driven by RHF, with an inline error message. */
|
|
106
|
+
export function ControlledCheckbox<T extends FieldValues>({
|
|
107
|
+
control,
|
|
108
|
+
name,
|
|
109
|
+
rules,
|
|
110
|
+
label,
|
|
111
|
+
}: BaseControlledProps<T>) {
|
|
112
|
+
return (
|
|
113
|
+
<Controller
|
|
114
|
+
control={control}
|
|
115
|
+
name={name}
|
|
116
|
+
rules={rules}
|
|
117
|
+
render={({ field, fieldState }) => (
|
|
118
|
+
<View className="gap-1">
|
|
119
|
+
<Checkbox
|
|
120
|
+
label={label}
|
|
121
|
+
isChecked={Boolean(field.value)}
|
|
122
|
+
onCheckedChange={field.onChange}
|
|
123
|
+
isInvalid={!!fieldState.error}
|
|
124
|
+
/>
|
|
125
|
+
{fieldState.error && (
|
|
126
|
+
<Text className="text-xs text-status-error">{fieldState.error.message}</Text>
|
|
127
|
+
)}
|
|
128
|
+
</View>
|
|
129
|
+
)}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ControlledRadioGroupProps<T extends FieldValues> extends BaseControlledProps<T> {
|
|
135
|
+
options: SelectOption[]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `RadioGroup` + `Radio` options driven by RHF, with an inline error message. */
|
|
139
|
+
export function ControlledRadioGroup<T extends FieldValues>({
|
|
140
|
+
control,
|
|
141
|
+
name,
|
|
142
|
+
rules,
|
|
143
|
+
label,
|
|
144
|
+
options,
|
|
145
|
+
}: ControlledRadioGroupProps<T>) {
|
|
146
|
+
return (
|
|
147
|
+
<Controller
|
|
148
|
+
control={control}
|
|
149
|
+
name={name}
|
|
150
|
+
rules={rules}
|
|
151
|
+
render={({ field, fieldState }) => (
|
|
152
|
+
<View className="gap-1">
|
|
153
|
+
<RadioGroup
|
|
154
|
+
label={label}
|
|
155
|
+
value={(field.value as string | null | undefined) ?? null}
|
|
156
|
+
onChange={field.onChange}
|
|
157
|
+
>
|
|
158
|
+
{options.map((option) => (
|
|
159
|
+
<Radio key={option.value} value={option.value}>
|
|
160
|
+
{option.label}
|
|
161
|
+
</Radio>
|
|
162
|
+
))}
|
|
163
|
+
</RadioGroup>
|
|
164
|
+
{fieldState.error && (
|
|
165
|
+
<Text className="text-xs text-status-error">{fieldState.error.message}</Text>
|
|
166
|
+
)}
|
|
167
|
+
</View>
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import { RhfContactForm, type ContactFormValues } from './RhfContactForm'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Examples showing how a consumer drives the styled form primitives
|
|
8
|
+
* (`FormField`, `Input`, `Select`, `Checkbox`, `Radio`) with
|
|
9
|
+
* [react-hook-form](https://react-hook-form.com/) using the canonical
|
|
10
|
+
* `Controller` render-prop idiom. See `ControlledFields.tsx` for the adapters.
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta<typeof RhfContactForm> = {
|
|
13
|
+
title: 'Examples/React Hook Form',
|
|
14
|
+
component: RhfContactForm,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'react-hook-form owns value + validation state and hands it to the ' +
|
|
21
|
+
'already-styled controlled components via `Controller`. No component ' +
|
|
22
|
+
'API is modified — this is pure integration wiring.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default meta
|
|
29
|
+
type Story = StoryObj<typeof RhfContactForm>
|
|
30
|
+
|
|
31
|
+
/** A complete form: type, select, pick a plan, accept terms, then submit. */
|
|
32
|
+
export const ContactForm: Story = {
|
|
33
|
+
render: function Render() {
|
|
34
|
+
const [submitted, setSubmitted] = React.useState<ContactFormValues | null>(null)
|
|
35
|
+
return (
|
|
36
|
+
<View style={{ gap: 16 }}>
|
|
37
|
+
<RhfContactForm onSubmit={setSubmitted} />
|
|
38
|
+
{submitted && (
|
|
39
|
+
<Text className="text-sm text-text-secondary">
|
|
40
|
+
Submitted: {JSON.stringify(submitted)}
|
|
41
|
+
</Text>
|
|
42
|
+
)}
|
|
43
|
+
</View>
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Submitting empty surfaces per-field validation errors from RHF rules. */
|
|
49
|
+
export const ValidationErrors: Story = {
|
|
50
|
+
render: () => <RhfContactForm />,
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
3
|
+
import { RhfContactForm } from './RhfContactForm'
|
|
4
|
+
|
|
5
|
+
describe('react-hook-form integration', () => {
|
|
6
|
+
it('submits the collected values from every controlled component', async () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const onSubmit = vi.fn()
|
|
9
|
+
render(<RhfContactForm onSubmit={onSubmit} />)
|
|
10
|
+
|
|
11
|
+
// Act
|
|
12
|
+
fireEvent.change(screen.getByPlaceholderText('Ada Lovelace'), {
|
|
13
|
+
target: { value: 'Grace Hopper' },
|
|
14
|
+
})
|
|
15
|
+
fireEvent.click(screen.getByText('Select a role...'))
|
|
16
|
+
fireEvent.click(screen.getByText('Designer'))
|
|
17
|
+
fireEvent.click(screen.getByText('Pro'))
|
|
18
|
+
fireEvent.click(screen.getByRole('checkbox'))
|
|
19
|
+
fireEvent.click(screen.getByText('Submit'))
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
|
|
23
|
+
expect(onSubmit.mock.calls[0][0]).toEqual({
|
|
24
|
+
fullName: 'Grace Hopper',
|
|
25
|
+
role: 'designer',
|
|
26
|
+
plan: 'pro',
|
|
27
|
+
acceptedTerms: true,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('surfaces validation errors and blocks submit when required fields are empty', async () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const onSubmit = vi.fn()
|
|
34
|
+
render(<RhfContactForm onSubmit={onSubmit} />)
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
fireEvent.click(screen.getByText('Submit'))
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(await screen.findByText('Full name is required')).toBeInTheDocument()
|
|
41
|
+
expect(screen.getByText('Please choose a role')).toBeInTheDocument()
|
|
42
|
+
expect(screen.getByText('Please pick a plan')).toBeInTheDocument()
|
|
43
|
+
expect(screen.getByText('You must accept the terms')).toBeInTheDocument()
|
|
44
|
+
expect(onSubmit).not.toHaveBeenCalled()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('clears a field error once the value becomes valid', async () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
render(<RhfContactForm />)
|
|
50
|
+
fireEvent.click(screen.getByText('Submit'))
|
|
51
|
+
expect(await screen.findByText('Full name is required')).toBeInTheDocument()
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
fireEvent.change(screen.getByPlaceholderText('Ada Lovelace'), {
|
|
55
|
+
target: { value: 'Katherine Johnson' },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
await waitFor(() => expect(screen.queryByText('Full name is required')).not.toBeInTheDocument())
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { View } from 'react-native'
|
|
2
|
+
import { useForm, type SubmitHandler } from 'react-hook-form'
|
|
3
|
+
import { Button, ButtonText } from '../../components/ui/button'
|
|
4
|
+
import { type SelectOption } from '../../components/ui/select'
|
|
5
|
+
import {
|
|
6
|
+
ControlledInput,
|
|
7
|
+
ControlledSelect,
|
|
8
|
+
ControlledCheckbox,
|
|
9
|
+
ControlledRadioGroup,
|
|
10
|
+
} from './ControlledFields'
|
|
11
|
+
|
|
12
|
+
export interface ContactFormValues {
|
|
13
|
+
fullName: string
|
|
14
|
+
role: string
|
|
15
|
+
plan: string
|
|
16
|
+
acceptedTerms: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const roleOptions: SelectOption[] = [
|
|
20
|
+
{ value: 'engineer', label: 'Engineer' },
|
|
21
|
+
{ value: 'designer', label: 'Designer' },
|
|
22
|
+
{ value: 'manager', label: 'Manager' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const planOptions: SelectOption[] = [
|
|
26
|
+
{ value: 'free', label: 'Free' },
|
|
27
|
+
{ value: 'pro', label: 'Pro' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const defaultValues: ContactFormValues = {
|
|
31
|
+
fullName: '',
|
|
32
|
+
role: '',
|
|
33
|
+
plan: '',
|
|
34
|
+
acceptedTerms: false,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RhfContactFormProps {
|
|
38
|
+
/** Called with validated form values on a successful submit. */
|
|
39
|
+
onSubmit?: SubmitHandler<ContactFormValues>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* End-to-end example wiring the styled form primitives to react-hook-form.
|
|
44
|
+
*
|
|
45
|
+
* Demonstrates the `Controller` idiom across `FormField`, `Input`, `Select`,
|
|
46
|
+
* `Checkbox`, and `Radio` — a single `useForm` instance owns state, validation,
|
|
47
|
+
* and submission. No component API is modified.
|
|
48
|
+
*/
|
|
49
|
+
export function RhfContactForm({ onSubmit }: RhfContactFormProps) {
|
|
50
|
+
const { control, handleSubmit } = useForm<ContactFormValues>({
|
|
51
|
+
defaultValues,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const handleValid: SubmitHandler<ContactFormValues> = (values) => {
|
|
55
|
+
onSubmit?.(values)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View className="gap-4" style={{ maxWidth: 360 }}>
|
|
60
|
+
<ControlledInput
|
|
61
|
+
control={control}
|
|
62
|
+
name="fullName"
|
|
63
|
+
label="Full name"
|
|
64
|
+
isRequired
|
|
65
|
+
placeholder="Ada Lovelace"
|
|
66
|
+
rules={{ required: 'Full name is required' }}
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<ControlledSelect
|
|
70
|
+
control={control}
|
|
71
|
+
name="role"
|
|
72
|
+
label="Role"
|
|
73
|
+
isRequired
|
|
74
|
+
options={roleOptions}
|
|
75
|
+
placeholder="Select a role..."
|
|
76
|
+
rules={{ required: 'Please choose a role' }}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<ControlledRadioGroup
|
|
80
|
+
control={control}
|
|
81
|
+
name="plan"
|
|
82
|
+
label="Plan"
|
|
83
|
+
options={planOptions}
|
|
84
|
+
rules={{ required: 'Please pick a plan' }}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<ControlledCheckbox
|
|
88
|
+
control={control}
|
|
89
|
+
name="acceptedTerms"
|
|
90
|
+
label="I accept the terms and conditions"
|
|
91
|
+
rules={{ required: 'You must accept the terms' }}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<Button onPress={handleSubmit(handleValid)}>
|
|
95
|
+
<ButtonText>Submit</ButtonText>
|
|
96
|
+
</Button>
|
|
97
|
+
</View>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -24,7 +24,7 @@ function PresetSwitcher({
|
|
|
24
24
|
onSwitch: (name: string) => void
|
|
25
25
|
}) {
|
|
26
26
|
return (
|
|
27
|
-
<View className="flex-row items-center gap-3 p-4 bg-surface-elevated rounded-lg border border-border
|
|
27
|
+
<View className="flex-row items-center gap-3 p-4 bg-surface-elevated rounded-lg border border-border">
|
|
28
28
|
<Text className="text-text-secondary text-sm font-medium">Active Preset:</Text>
|
|
29
29
|
{Object.keys(presets).map((name) => (
|
|
30
30
|
<Pressable
|
|
@@ -33,7 +33,7 @@ function PresetSwitcher({
|
|
|
33
33
|
className={`px-4 py-2 rounded-md border ${
|
|
34
34
|
activePreset === name
|
|
35
35
|
? 'bg-brand-primary border-brand-primary'
|
|
36
|
-
: 'bg-surface-base border-border
|
|
36
|
+
: 'bg-surface-base border-border web:hover:bg-interactive-hover'
|
|
37
37
|
}`}
|
|
38
38
|
>
|
|
39
39
|
<Text
|
|
@@ -272,7 +272,7 @@ function TypographyShowcase() {
|
|
|
272
272
|
</Section>
|
|
273
273
|
|
|
274
274
|
<Section title="Monospace Font (font-mono)">
|
|
275
|
-
<View className="bg-surface-elevated rounded-lg p-4 border border-border
|
|
275
|
+
<View className="bg-surface-elevated rounded-lg p-4 border border-border">
|
|
276
276
|
<Text className="text-sm text-text-primary font-mono">
|
|
277
277
|
{'const theme = applyThemePreset(audiobookPreset)'}
|
|
278
278
|
</Text>
|
|
@@ -368,13 +368,13 @@ function ColorPaletteShowcase() {
|
|
|
368
368
|
|
|
369
369
|
<Section title="Text Colors">
|
|
370
370
|
<View className="flex-row gap-3 flex-wrap">
|
|
371
|
-
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border
|
|
371
|
+
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border">
|
|
372
372
|
<Text className="text-xs font-mono font-medium text-text-primary">text-primary</Text>
|
|
373
373
|
</View>
|
|
374
|
-
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border
|
|
374
|
+
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border">
|
|
375
375
|
<Text className="text-xs font-mono font-medium text-text-secondary">text-secondary</Text>
|
|
376
376
|
</View>
|
|
377
|
-
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border
|
|
377
|
+
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border">
|
|
378
378
|
<Text className="text-xs font-mono font-medium text-text-tertiary">text-tertiary</Text>
|
|
379
379
|
</View>
|
|
380
380
|
</View>
|
|
@@ -382,25 +382,25 @@ function ColorPaletteShowcase() {
|
|
|
382
382
|
|
|
383
383
|
<Section title="Surface Colors">
|
|
384
384
|
<View className="flex-row gap-3 flex-wrap">
|
|
385
|
-
<ColorSwatch label="surface-base" className="bg-surface-base border border-border
|
|
386
|
-
<ColorSwatch label="surface-elevated" className="bg-surface-elevated border border-border
|
|
387
|
-
<ColorSwatch label="surface-raised" className="bg-surface-raised border border-border
|
|
388
|
-
<ColorSwatch label="surface-overlay" className="bg-surface-overlay border border-border
|
|
389
|
-
<ColorSwatch label="surface-input" className="bg-surface-input border border-border
|
|
385
|
+
<ColorSwatch label="surface-base" className="bg-surface-base border border-border" textClass="text-text-primary" />
|
|
386
|
+
<ColorSwatch label="surface-elevated" className="bg-surface-elevated border border-border" textClass="text-text-primary" />
|
|
387
|
+
<ColorSwatch label="surface-raised" className="bg-surface-raised border border-border" textClass="text-text-primary" />
|
|
388
|
+
<ColorSwatch label="surface-overlay" className="bg-surface-overlay border border-border" textClass="text-text-primary" />
|
|
389
|
+
<ColorSwatch label="surface-input" className="bg-surface-input border border-border" textClass="text-text-primary" />
|
|
390
390
|
</View>
|
|
391
391
|
</Section>
|
|
392
392
|
|
|
393
393
|
<Section title="Background Colors">
|
|
394
394
|
<View className="flex-row gap-3 flex-wrap">
|
|
395
|
-
<ColorSwatch label="background-base" className="bg-background-base border border-border
|
|
396
|
-
<ColorSwatch label="background-default" className="bg-background-default border border-border
|
|
397
|
-
<ColorSwatch label="background-subtle" className="bg-background-subtle border border-border
|
|
395
|
+
<ColorSwatch label="background-base" className="bg-background-base border border-border" textClass="text-text-primary" />
|
|
396
|
+
<ColorSwatch label="background-default" className="bg-background-default border border-border" textClass="text-text-primary" />
|
|
397
|
+
<ColorSwatch label="background-subtle" className="bg-background-subtle border border-border" textClass="text-text-primary" />
|
|
398
398
|
</View>
|
|
399
399
|
</Section>
|
|
400
400
|
|
|
401
401
|
<Section title="Border Colors">
|
|
402
402
|
<View className="flex-row gap-3 flex-wrap">
|
|
403
|
-
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border
|
|
403
|
+
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border">
|
|
404
404
|
<Text className="text-xs font-mono font-medium text-text-primary">border-default</Text>
|
|
405
405
|
</View>
|
|
406
406
|
<View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border-subtle">
|
package/src/theme/index.ts
CHANGED
|
@@ -3,11 +3,12 @@ export * from './tokens'
|
|
|
3
3
|
export * from './config'
|
|
4
4
|
export * from './shadows'
|
|
5
5
|
export * from './elevation'
|
|
6
|
+
export * from './manifest'
|
|
6
7
|
|
|
7
8
|
// Re-export commonly used items
|
|
8
9
|
export { getSemanticColors, type ThemeMode } from './tokens/semantic'
|
|
9
10
|
export { getThemeCSSVars, gluestackConfig } from './config'
|
|
10
|
-
export {
|
|
11
|
+
export {
|
|
11
12
|
neumorphicShadows,
|
|
12
13
|
// Color math utilities (HSV-based)
|
|
13
14
|
lighten,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"component": "button",
|
|
3
|
+
"description": "Illustrative example conforming to css-property-manifest.schema.json. Values are derived from Button.tsx's variant/color/size style maps and the semantic token definitions they reference, not measured via getComputedStyle — a follow-up ticket generates verified manifests from the rendered Storybook DOM.",
|
|
4
|
+
"baseVariant": { "variant": "solid", "color": "primary", "size": "md" },
|
|
5
|
+
"properties": [
|
|
6
|
+
{
|
|
7
|
+
"property": "backgroundColor",
|
|
8
|
+
"token": "brand-primary",
|
|
9
|
+
"cssVariable": "--color-brand-primary",
|
|
10
|
+
"resolvedValue": "rgb(255, 121, 0)",
|
|
11
|
+
"variantOverrides": [
|
|
12
|
+
{
|
|
13
|
+
"variant": { "variant": "outline" },
|
|
14
|
+
"token": null,
|
|
15
|
+
"cssVariable": null,
|
|
16
|
+
"resolvedValue": "rgba(0, 0, 0, 0)"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"variant": { "variant": "solid", "color": "error" },
|
|
20
|
+
"token": "status-error",
|
|
21
|
+
"cssVariable": "--color-status-error",
|
|
22
|
+
"resolvedValue": "rgb(209, 67, 67)"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"property": "color",
|
|
28
|
+
"token": null,
|
|
29
|
+
"resolvedValue": "rgb(255, 255, 255)",
|
|
30
|
+
"variantOverrides": [
|
|
31
|
+
{
|
|
32
|
+
"variant": { "variant": "outline" },
|
|
33
|
+
"token": "brand-primary",
|
|
34
|
+
"cssVariable": "--color-brand-primary",
|
|
35
|
+
"resolvedValue": "rgb(255, 121, 0)"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"property": "borderColor",
|
|
41
|
+
"token": null,
|
|
42
|
+
"resolvedValue": "rgba(0, 0, 0, 0)",
|
|
43
|
+
"variantOverrides": [
|
|
44
|
+
{
|
|
45
|
+
"variant": { "variant": "outline" },
|
|
46
|
+
"token": "brand-primary",
|
|
47
|
+
"cssVariable": "--color-brand-primary",
|
|
48
|
+
"resolvedValue": "rgb(255, 121, 0)"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"property": "borderWidth",
|
|
54
|
+
"token": null,
|
|
55
|
+
"resolvedValue": "0px",
|
|
56
|
+
"variantOverrides": [
|
|
57
|
+
{
|
|
58
|
+
"variant": { "variant": "outline" },
|
|
59
|
+
"token": null,
|
|
60
|
+
"resolvedValue": "2px"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"property": "borderRadius",
|
|
66
|
+
"token": null,
|
|
67
|
+
"resolvedValue": "6px"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"property": "paddingLeft",
|
|
71
|
+
"token": null,
|
|
72
|
+
"resolvedValue": "20px",
|
|
73
|
+
"variantOverrides": [
|
|
74
|
+
{ "variant": { "size": "sm" }, "token": null, "resolvedValue": "16px" },
|
|
75
|
+
{ "variant": { "size": "lg" }, "token": null, "resolvedValue": "24px" }
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://titan-design.dev/schemas/css-property-manifest.schema.json",
|
|
4
|
+
"title": "Component CSS Property Manifest",
|
|
5
|
+
"description": "Per-component record of visual CSS properties, the design token each one resolves from, the expected computed style value, and any per-variant overrides. Generated manifests serve as fixtures for Playwright computed-style assertions (see packages/ui/tests/visual).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["component", "properties"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"component": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
13
|
+
"description": "Component identifier matching its src/components/ui/{component} directory and Storybook id (e.g. \"button\")."
|
|
14
|
+
},
|
|
15
|
+
"description": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Optional human-readable note about what this manifest covers."
|
|
18
|
+
},
|
|
19
|
+
"baseVariant": {
|
|
20
|
+
"$ref": "#/$defs/variantAxes",
|
|
21
|
+
"description": "The variant axis values (e.g. { \"variant\": \"solid\", \"color\": \"primary\", \"size\": \"md\" }) that each property's top-level resolvedValue was captured against. Axis names must match the component's own prop names."
|
|
22
|
+
},
|
|
23
|
+
"properties": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"items": { "$ref": "#/$defs/cssPropertyEntry" },
|
|
27
|
+
"description": "One entry per visual CSS property tracked for this component."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"$defs": {
|
|
31
|
+
"variantAxes": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"minProperties": 1,
|
|
34
|
+
"additionalProperties": { "type": "string" },
|
|
35
|
+
"description": "A set of variant axis name/value pairs (e.g. variant, color, size, state) as accepted by the component's props."
|
|
36
|
+
},
|
|
37
|
+
"cssPropertyEntry": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"required": ["property", "token", "resolvedValue"],
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"properties": {
|
|
42
|
+
"property": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"pattern": "^[a-zA-Z]+$",
|
|
45
|
+
"description": "CSS property name in camelCase as returned by window.getComputedStyle (e.g. backgroundColor, borderColor, fontSize)."
|
|
46
|
+
},
|
|
47
|
+
"token": {
|
|
48
|
+
"type": ["string", "null"],
|
|
49
|
+
"description": "Semantic design token key this property is sourced from (e.g. \"brand-primary\"), matching a key in packages/ui/src/theme/tokens/semantic.ts. Use null when the value is a literal not backed by a token."
|
|
50
|
+
},
|
|
51
|
+
"cssVariable": {
|
|
52
|
+
"type": ["string", "null"],
|
|
53
|
+
"description": "CSS custom property backing the token (e.g. \"--color-brand-primary\"), as declared in packages/ui/src/theme/global.css. Omit or null when no CSS variable is involved."
|
|
54
|
+
},
|
|
55
|
+
"resolvedValue": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"description": "Expected computed style value for baseVariant, in the format getComputedStyle returns it (e.g. \"rgb(255, 121, 0)\", \"8px\")."
|
|
59
|
+
},
|
|
60
|
+
"variantOverrides": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"default": [],
|
|
63
|
+
"items": { "$ref": "#/$defs/variantOverride" },
|
|
64
|
+
"description": "Overrides of token/resolvedValue for specific variant axis combinations, merged onto baseVariant."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"variantOverride": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"required": ["variant", "resolvedValue"],
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"properties": {
|
|
73
|
+
"variant": {
|
|
74
|
+
"$ref": "#/$defs/variantAxes",
|
|
75
|
+
"description": "Partial variant axis values that trigger this override (e.g. { \"variant\": \"outline\" } or { \"color\": \"error\", \"state\": \"hover\" })."
|
|
76
|
+
},
|
|
77
|
+
"token": {
|
|
78
|
+
"type": ["string", "null"],
|
|
79
|
+
"description": "Semantic token key for this override, or null for a literal value."
|
|
80
|
+
},
|
|
81
|
+
"cssVariable": {
|
|
82
|
+
"type": ["string", "null"],
|
|
83
|
+
"description": "CSS custom property backing the token for this override."
|
|
84
|
+
},
|
|
85
|
+
"resolvedValue": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"minLength": 1,
|
|
88
|
+
"description": "Expected computed style value when the variant axes match."
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|