@payfit/unity-components 2.55.13 → 2.55.15
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/esm/components/autocomplete/parts/AutocompletePanel.js +1 -1
- package/dist/esm/components/avatar/Avatar.context.js +1 -1
- package/dist/esm/components/bottom-sheet/parts/BottomSheetDragIndicator.js +1 -1
- package/dist/esm/components/breadcrumbs/Breadcrumbs.context.js +1 -1
- package/dist/esm/components/breadcrumbs/Breadcrumbs.js +1 -1
- package/dist/esm/components/carousel/Carousel.options.js +1 -1
- package/dist/esm/components/carousel/hooks/useCarouselState.js +8 -4
- package/dist/esm/components/client-side-pagination/ClientSidePagination.js +1 -1
- package/dist/esm/components/client-side-pagination/parts/RawPaginationLink.js +1 -1
- package/dist/esm/components/client-side-pagination/utils/pagination-window.js +18 -19
- package/dist/esm/components/code/Code.js +1 -1
- package/dist/esm/components/collapsible/Collapsible.js +1 -1
- package/dist/esm/components/dialog/test-utils.js +4 -2
- package/dist/esm/components/error-state/initConfig.js +1 -1
- package/dist/esm/components/filter/parts/FilterButton.js +1 -1
- package/dist/esm/components/filter/parts/FilterLabel.js +1 -1
- package/dist/esm/components/filter/utils/value-formatters.js +1 -1
- package/dist/esm/components/filter-toolbar/FilterToolbar.js +27 -27
- package/dist/esm/components/filter-toolbar/parts/AddFilterItem.js +1 -1
- package/dist/esm/components/form/Form.context.js +1 -1
- package/dist/esm/components/form-field/FormField.context.js +1 -1
- package/dist/esm/components/inline-field/parts/InlineFieldEditView.js +7 -4
- package/dist/esm/components/inline-field-group/InlineFieldGroup.js +14 -8
- package/dist/esm/components/multi-select/MultiSelect.js +2 -2
- package/dist/esm/components/pagination/utils/pagination-window.js +25 -26
- package/dist/esm/components/phone-number/parts/PhoneNumberItem.js +1 -1
- package/dist/esm/components/popover/Popover.js +1 -1
- package/dist/esm/components/radio-button-group/parts/RadioButton.js +1 -1
- package/dist/esm/components/select/parts/SelectOption.context.js +1 -1
- package/dist/esm/components/select-list/helpers.js +2 -2
- package/dist/esm/components/select-list/parts/SelectListOptGroup.js +1 -1
- package/dist/esm/components/selectable-card/internals/Content.js +1 -1
- package/dist/esm/components/selectable-card/internals/Description.js +1 -1
- package/dist/esm/components/selectable-card/internals/Illustration.js +1 -1
- package/dist/esm/components/selectable-card/internals/SelectedIndicator.js +1 -1
- package/dist/esm/components/side-panel/parts/SidePanelDragIndicator.js +1 -1
- package/dist/esm/components/stepper/Stepper.context.js +1 -1
- package/dist/esm/components/table/Table.context.js +1 -1
- package/dist/esm/components/table/hooks/useTableKeyboardNavigation.js +10 -8
- package/dist/esm/components/tabs/Tabs.context.js +1 -1
- package/dist/esm/components/task-menu/TaskMenu.context.js +1 -1
- package/dist/esm/components/timeline/Timeline.context.js +1 -1
- package/dist/esm/components/timeline/parts/TimelineStepContent.context.js +1 -1
- package/dist/esm/hooks/use-has-scroll.js +3 -1
- package/dist/esm/index.js +2 -2
- package/package.json +14 -14
- package/skills/unity-data-table/SKILL.md +24 -240
- package/skills/unity-data-table/references/patterns.md +237 -0
- package/skills/unity-find-component/SKILL.md +20 -105
- package/skills/unity-find-component/references/component-catalog.md +94 -0
- package/skills/unity-layout-and-styling/SKILL.md +14 -141
- package/skills/unity-layout-and-styling/references/patterns.md +139 -0
- package/skills/unity-migrate-from-midnight/SKILL.md +9 -9
- package/skills/unity-navigation/SKILL.md +12 -116
- package/skills/unity-navigation/references/patterns.md +115 -0
- package/skills/unity-overlays/SKILL.md +11 -132
- package/skills/unity-overlays/references/patterns.md +131 -0
- package/skills/unity-setup-feature-plugin/SKILL.md +4 -3
- package/skills/unity-tanstack-form/SKILL.md +15 -102
- package/skills/unity-tanstack-form/references/patterns.md +102 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Unity overlay patterns
|
|
2
|
+
|
|
3
|
+
### Dialog with DialogTrigger + DialogActions
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogActions,
|
|
10
|
+
DialogButton,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
} from '@payfit/unity-components'
|
|
15
|
+
|
|
16
|
+
export function ArchiveDialog({ onArchive }: { onArchive: () => void }) {
|
|
17
|
+
return (
|
|
18
|
+
<DialogTrigger>
|
|
19
|
+
<Button>Archive</Button>
|
|
20
|
+
<Dialog size="md">
|
|
21
|
+
<DialogTitle>Archive this employee?</DialogTitle>
|
|
22
|
+
<DialogContent>
|
|
23
|
+
They will no longer appear in active lists. You can restore them
|
|
24
|
+
later.
|
|
25
|
+
</DialogContent>
|
|
26
|
+
<DialogActions>
|
|
27
|
+
<DialogButton variant="close">Cancel</DialogButton>
|
|
28
|
+
<DialogButton variant="confirm" onPress={onArchive}>
|
|
29
|
+
Archive
|
|
30
|
+
</DialogButton>
|
|
31
|
+
</DialogActions>
|
|
32
|
+
</Dialog>
|
|
33
|
+
</DialogTrigger>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### SidePanel for detail views
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useState } from 'react'
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
Button,
|
|
45
|
+
SidePanel,
|
|
46
|
+
SidePanelContent,
|
|
47
|
+
SidePanelFooter,
|
|
48
|
+
SidePanelHeader,
|
|
49
|
+
} from '@payfit/unity-components'
|
|
50
|
+
|
|
51
|
+
export function EmployeeDetailPanel({ employee }: { employee: Employee }) {
|
|
52
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<Button onPress={() => setIsOpen(true)}>View details</Button>
|
|
57
|
+
<SidePanel isOpen={isOpen} onOpenChange={setIsOpen}>
|
|
58
|
+
<SidePanelHeader>{employee.name}</SidePanelHeader>
|
|
59
|
+
<SidePanelContent>
|
|
60
|
+
<p>{employee.position}</p>
|
|
61
|
+
<p>{employee.team}</p>
|
|
62
|
+
</SidePanelContent>
|
|
63
|
+
<SidePanelFooter>
|
|
64
|
+
<Button variant="secondary" onPress={() => setIsOpen(false)}>
|
|
65
|
+
Close
|
|
66
|
+
</Button>
|
|
67
|
+
</SidePanelFooter>
|
|
68
|
+
</SidePanel>
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Popover for interactive informational content
|
|
75
|
+
|
|
76
|
+
`title` is required (use `isTitleSrOnly` if you don't want it visible).
|
|
77
|
+
Children may be focusable; the Popover is non-modal so the page stays
|
|
78
|
+
interactive behind it.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { Button, Popover, PopoverTrigger } from '@payfit/unity-components'
|
|
82
|
+
|
|
83
|
+
export function HelpPopover() {
|
|
84
|
+
return (
|
|
85
|
+
<PopoverTrigger>
|
|
86
|
+
<Button variant="tertiary">What is a working agreement?</Button>
|
|
87
|
+
<Popover title="Working agreement">
|
|
88
|
+
<p>
|
|
89
|
+
A working agreement defines when, where and how an employee works. See
|
|
90
|
+
the <a href="/handbook/working-agreement">handbook</a> for details.
|
|
91
|
+
</p>
|
|
92
|
+
</Popover>
|
|
93
|
+
</PopoverTrigger>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Menu for action lists
|
|
99
|
+
|
|
100
|
+
`Menu` expects exactly two children: a trigger and a content block. Use
|
|
101
|
+
`MenuTrigger` / `MenuContent` / `RawMenuItem` for the standard composition.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import {
|
|
105
|
+
IconButton,
|
|
106
|
+
Menu,
|
|
107
|
+
MenuContent,
|
|
108
|
+
MenuTrigger,
|
|
109
|
+
RawMenuItem,
|
|
110
|
+
} from '@payfit/unity-components'
|
|
111
|
+
|
|
112
|
+
export function RowActions({
|
|
113
|
+
onEdit,
|
|
114
|
+
onArchive,
|
|
115
|
+
}: {
|
|
116
|
+
onEdit: () => void
|
|
117
|
+
onArchive: () => void
|
|
118
|
+
}) {
|
|
119
|
+
return (
|
|
120
|
+
<Menu>
|
|
121
|
+
<MenuTrigger>
|
|
122
|
+
<IconButton icon="MoreFilled" title="Actions" />
|
|
123
|
+
</MenuTrigger>
|
|
124
|
+
<MenuContent>
|
|
125
|
+
<RawMenuItem onAction={onEdit}>Edit</RawMenuItem>
|
|
126
|
+
<RawMenuItem onAction={onArchive}>Archive</RawMenuItem>
|
|
127
|
+
</MenuContent>
|
|
128
|
+
</Menu>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
@@ -7,9 +7,10 @@ description: >
|
|
|
7
7
|
→ I18nProvider (react-aria-components) → UnityIconsProvider. Import
|
|
8
8
|
@payfit/unity-themes/css/unity.css once. Merge @payfit/unity-components/i18n
|
|
9
9
|
JSON into the IntlProvider messages, keyed by locale.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
metadata:
|
|
11
|
+
type: lifecycle
|
|
12
|
+
library: '@payfit/unity-components'
|
|
13
|
+
library_version: '2.x'
|
|
13
14
|
sources:
|
|
14
15
|
- 'PayFit/hr-apps:libs/shared/unity/components/src/docs/tutorials/Getting Started.mdx'
|
|
15
16
|
- 'PayFit/hr-apps:libs/shared/unity/icons/src/components/icons-provider/UnityIconsProvider.tsx'
|
|
@@ -4,9 +4,10 @@ description: >
|
|
|
4
4
|
Load when building or migrating Unity forms. Use useTanstackUnityForm with
|
|
5
5
|
schema validation and prefer Tanstack Form field APIs for composed fields,
|
|
6
6
|
custom layouts, and validation behavior.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
metadata:
|
|
8
|
+
type: core
|
|
9
|
+
library: '@payfit/unity-components'
|
|
10
|
+
library_version: '2.x'
|
|
10
11
|
sources:
|
|
11
12
|
- 'PayFit/hr-apps:libs/shared/unity/components/src/hooks/use-tanstack-form.tsx'
|
|
12
13
|
- 'PayFit/hr-apps:libs/shared/unity/components/src/hooks/use-form.tsx'
|
|
@@ -61,103 +62,14 @@ Wrapping order is load-bearing: `form.AppForm` provides the form context, `form.
|
|
|
61
62
|
|
|
62
63
|
## Core Patterns
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
- Prefer composed `field.*Field` components for ordinary fields.
|
|
66
|
+
- Use the atomic field parts only when custom layout or control composition is
|
|
67
|
+
required.
|
|
68
|
+
- Default validation to blur; use dynamic validation for fields that need
|
|
69
|
+
blur-then-change behavior.
|
|
70
|
+
- Narrow every `form.Subscribe` with a selector.
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
<form.AppField name="firstName">
|
|
70
|
-
{field => (
|
|
71
|
-
<field.TextField
|
|
72
|
-
label="First name"
|
|
73
|
-
helperText="As it appears on your ID"
|
|
74
|
-
isRequired
|
|
75
|
-
/>
|
|
76
|
-
)}
|
|
77
|
-
</form.AppField>
|
|
78
|
-
|
|
79
|
-
<form.AppField name="country">
|
|
80
|
-
{field => (
|
|
81
|
-
<field.SelectField
|
|
82
|
-
label="Country"
|
|
83
|
-
options={[
|
|
84
|
-
{ value: 'fr', label: 'France' },
|
|
85
|
-
{ value: 'es', label: 'Spain' },
|
|
86
|
-
]}
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
89
|
-
</form.AppField>
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Atomic API (only when customizing layout/parts)
|
|
93
|
-
|
|
94
|
-
Reach for Atomic when you need to interleave custom content between the label and the input, or swap an input for a non-standard control. Wraps every part in `field.Field`.
|
|
95
|
-
|
|
96
|
-
```tsx
|
|
97
|
-
<form.AppField name="password">
|
|
98
|
-
{field => (
|
|
99
|
-
<field.Field>
|
|
100
|
-
<field.FieldLabel isRequired>Password</field.FieldLabel>
|
|
101
|
-
<field.FieldHelperText>Enter a strong password</field.FieldHelperText>
|
|
102
|
-
<field.TextInput type="password" />
|
|
103
|
-
<form.Subscribe selector={s => s.values.password}>
|
|
104
|
-
{password => (
|
|
105
|
-
<Text variant="bodySmallStrong">Length: {password.length}</Text>
|
|
106
|
-
)}
|
|
107
|
-
</form.Subscribe>
|
|
108
|
-
<field.FieldFeedbackText />
|
|
109
|
-
</field.Field>
|
|
110
|
-
)}
|
|
111
|
-
</form.AppField>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Validation timing
|
|
115
|
-
|
|
116
|
-
`validators.onBlur` is the default; use `onChange` only for fields that need live feedback (password strength meter, search-as-you-type). `fieldRevalidateLogic` gives "blur until first error, then change" UX without polluting form-level validators.
|
|
117
|
-
|
|
118
|
-
```tsx
|
|
119
|
-
import { fieldRevalidateLogic, useTanstackUnityForm } from '@payfit/unity-components'
|
|
120
|
-
|
|
121
|
-
const form = useTanstackUnityForm({
|
|
122
|
-
defaultValues: { email: '', password: '' },
|
|
123
|
-
validators: { onBlur: z.object({ email: z.email() }) },
|
|
124
|
-
validationLogic: fieldRevalidateLogic({
|
|
125
|
-
whenPristine: 'blur',
|
|
126
|
-
whenDirty: 'change',
|
|
127
|
-
fields: ['password'],
|
|
128
|
-
}),
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
<form.AppField
|
|
132
|
-
name="password"
|
|
133
|
-
validators={{
|
|
134
|
-
onDynamic: ({ value }) =>
|
|
135
|
-
value.length < 8 ? 'Password too short' : undefined,
|
|
136
|
-
}}
|
|
137
|
-
>
|
|
138
|
-
{field => <field.PasswordField label="Password" />}
|
|
139
|
-
</form.AppField>
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Fields listed in `fieldRevalidateLogic.fields` MUST use `onDynamic`/`onDynamicAsync` as their sole validator and MUST NOT also appear in a form-level schema, or stale errors will linger.
|
|
143
|
-
|
|
144
|
-
### Optimal subscription with form.Subscribe + selector
|
|
145
|
-
|
|
146
|
-
Always pass a `selector` to `form.Subscribe`. A bare children-only subscription re-renders on every keystroke anywhere in the form.
|
|
147
|
-
|
|
148
|
-
```tsx
|
|
149
|
-
<form.Subscribe selector={s => s.values.password}>
|
|
150
|
-
{password => <Text>Length: {password.length}</Text>}
|
|
151
|
-
</form.Subscribe>
|
|
152
|
-
|
|
153
|
-
<form.Subscribe selector={s => [s.canSubmit, s.isSubmitting] as const}>
|
|
154
|
-
{([canSubmit, isSubmitting]) => (
|
|
155
|
-
<Button type="submit" isDisabled={!canSubmit} isLoading={isSubmitting}>
|
|
156
|
-
Submit
|
|
157
|
-
</Button>
|
|
158
|
-
)}
|
|
159
|
-
</form.Subscribe>
|
|
160
|
-
```
|
|
72
|
+
Read [references/patterns.md](references/patterns.md) for complete examples.
|
|
161
73
|
|
|
162
74
|
## Common Mistakes
|
|
163
75
|
|
|
@@ -181,9 +93,10 @@ const form = useTanstackUnityForm({ validators: { onBlur: schema } })
|
|
|
181
93
|
|
|
182
94
|
The legacy `use-form` hook is `@deprecated` but still exported; agents trained on older code reach for it and end up mixing RHF Controller with Tanstack field components, which breaks at runtime.
|
|
183
95
|
|
|
184
|
-
|
|
96
|
+
The legacy hook remains exported for compatibility but is deprecated. Do not
|
|
97
|
+
author new code with it.
|
|
185
98
|
|
|
186
|
-
Source: libs/shared/unity/components/src/hooks/use-form.tsx:79 (@deprecated JSDoc)
|
|
99
|
+
Source: libs/shared/unity/components/src/hooks/use-form.tsx:79 (@deprecated JSDoc)
|
|
187
100
|
|
|
188
101
|
### CRITICAL Omit form.AppForm or form.AppField wrapping
|
|
189
102
|
|
|
@@ -329,7 +242,7 @@ Correct:
|
|
|
329
242
|
|
|
330
243
|
`field.TextField` is the Composed API and already includes label/input/feedback; wrapping it in `field.Field` + `field.FieldLabel` double-wraps and breaks layout + a11y. Default to Composed; reach for Atomic only when you must customize the field's layout or swap a part — never as the standard pattern.
|
|
331
244
|
|
|
332
|
-
Source: TanstackTextField.tsx
|
|
245
|
+
Source: TanstackTextField.tsx; TanstackFormField.tsx and its parts
|
|
333
246
|
|
|
334
247
|
## References
|
|
335
248
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Unity TanStack Form patterns
|
|
2
|
+
|
|
3
|
+
### Composed API (default)
|
|
4
|
+
|
|
5
|
+
Drop a single bound field component inside `<form.AppField>`. It bundles label, input, helper text, feedback, and a11y wiring.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<form.AppField name="firstName">
|
|
9
|
+
{field => (
|
|
10
|
+
<field.TextField
|
|
11
|
+
label="First name"
|
|
12
|
+
helperText="As it appears on your ID"
|
|
13
|
+
isRequired
|
|
14
|
+
/>
|
|
15
|
+
)}
|
|
16
|
+
</form.AppField>
|
|
17
|
+
|
|
18
|
+
<form.AppField name="country">
|
|
19
|
+
{field => (
|
|
20
|
+
<field.SelectField
|
|
21
|
+
label="Country"
|
|
22
|
+
options={[
|
|
23
|
+
{ value: 'fr', label: 'France' },
|
|
24
|
+
{ value: 'es', label: 'Spain' },
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
</form.AppField>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Atomic API (only when customizing layout/parts)
|
|
32
|
+
|
|
33
|
+
Reach for Atomic when you need to interleave custom content between the label and the input, or swap an input for a non-standard control. Wraps every part in `field.Field`.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<form.AppField name="password">
|
|
37
|
+
{field => (
|
|
38
|
+
<field.Field>
|
|
39
|
+
<field.FieldLabel isRequired>Password</field.FieldLabel>
|
|
40
|
+
<field.FieldHelperText>Enter a strong password</field.FieldHelperText>
|
|
41
|
+
<field.TextInput type="password" />
|
|
42
|
+
<form.Subscribe selector={s => s.values.password}>
|
|
43
|
+
{password => (
|
|
44
|
+
<Text variant="bodySmallStrong">Length: {password.length}</Text>
|
|
45
|
+
)}
|
|
46
|
+
</form.Subscribe>
|
|
47
|
+
<field.FieldFeedbackText />
|
|
48
|
+
</field.Field>
|
|
49
|
+
)}
|
|
50
|
+
</form.AppField>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Validation timing
|
|
54
|
+
|
|
55
|
+
`validators.onBlur` is the default; use `onChange` only for fields that need live feedback (password strength meter, search-as-you-type). `fieldRevalidateLogic` gives "blur until first error, then change" UX without polluting form-level validators.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { fieldRevalidateLogic, useTanstackUnityForm } from '@payfit/unity-components'
|
|
59
|
+
|
|
60
|
+
const form = useTanstackUnityForm({
|
|
61
|
+
defaultValues: { email: '', password: '' },
|
|
62
|
+
validators: { onBlur: z.object({ email: z.email() }) },
|
|
63
|
+
validationLogic: fieldRevalidateLogic({
|
|
64
|
+
whenPristine: 'blur',
|
|
65
|
+
whenDirty: 'change',
|
|
66
|
+
fields: ['password'],
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
<form.AppField
|
|
71
|
+
name="password"
|
|
72
|
+
validators={{
|
|
73
|
+
onDynamic: ({ value }) =>
|
|
74
|
+
value.length < 8 ? 'Password too short' : undefined,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{field => <field.PasswordField label="Password" />}
|
|
78
|
+
</form.AppField>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For fields listed in `fieldRevalidateLogic.fields`, use
|
|
82
|
+
`onDynamic`/`onDynamicAsync` as the sole validator and omit those fields from
|
|
83
|
+
the form-level schema; combining both leaves stale errors.
|
|
84
|
+
|
|
85
|
+
### Optimal subscription with form.Subscribe + selector
|
|
86
|
+
|
|
87
|
+
Pass a `selector` to `form.Subscribe` so it observes only the state needed by
|
|
88
|
+
its children. A bare subscription re-renders on every form update.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<form.Subscribe selector={s => s.values.password}>
|
|
92
|
+
{password => <Text>Length: {password.length}</Text>}
|
|
93
|
+
</form.Subscribe>
|
|
94
|
+
|
|
95
|
+
<form.Subscribe selector={s => [s.canSubmit, s.isSubmitting] as const}>
|
|
96
|
+
{([canSubmit, isSubmitting]) => (
|
|
97
|
+
<Button type="submit" isDisabled={!canSubmit} isLoading={isSubmitting}>
|
|
98
|
+
Submit
|
|
99
|
+
</Button>
|
|
100
|
+
)}
|
|
101
|
+
</form.Subscribe>
|
|
102
|
+
```
|