@mindlogic-ai/logician-ui 4.0.0-alpha.13 → 4.0.0-alpha.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/components/Button/Button.styles.d.ts.map +1 -1
- package/dist/components/Button/Button.styles.js +18 -12
- package/dist/components/Button/Button.styles.js.map +1 -1
- package/dist/components/Button/Button.styles.mjs +18 -12
- package/dist/components/Button/Button.styles.mjs.map +1 -1
- package/dist/components/Card/Card.styles.d.ts +4 -0
- package/dist/components/Card/Card.styles.d.ts.map +1 -1
- package/dist/components/Card/Card.styles.js +8 -0
- package/dist/components/Card/Card.styles.js.map +1 -1
- package/dist/components/Card/Card.styles.mjs +8 -0
- package/dist/components/Card/Card.styles.mjs.map +1 -1
- package/dist/components/Card/Card.types.d.ts +1 -1
- package/dist/components/Card/Card.types.d.ts.map +1 -1
- package/dist/components/Form/FieldError/FieldError.d.ts +15 -0
- package/dist/components/Form/FieldError/FieldError.d.ts.map +1 -0
- package/dist/components/Form/FieldError/FieldError.js +22 -0
- package/dist/components/Form/FieldError/FieldError.js.map +1 -0
- package/dist/components/Form/FieldError/FieldError.mjs +20 -0
- package/dist/components/Form/FieldError/FieldError.mjs.map +1 -0
- package/dist/components/Form/FieldError/index.d.ts +3 -0
- package/dist/components/Form/FieldError/index.d.ts.map +1 -0
- package/dist/components/Form/FieldHelp/FieldHelp.d.ts +14 -0
- package/dist/components/Form/FieldHelp/FieldHelp.d.ts.map +1 -0
- package/dist/components/Form/FieldHelp/FieldHelp.js +19 -0
- package/dist/components/Form/FieldHelp/FieldHelp.js.map +1 -0
- package/dist/components/Form/FieldHelp/FieldHelp.mjs +17 -0
- package/dist/components/Form/FieldHelp/FieldHelp.mjs.map +1 -0
- package/dist/components/Form/FieldHelp/index.d.ts +3 -0
- package/dist/components/Form/FieldHelp/index.d.ts.map +1 -0
- package/dist/components/Form/FieldLabel/FieldLabel.d.ts +12 -0
- package/dist/components/Form/FieldLabel/FieldLabel.d.ts.map +1 -0
- package/dist/components/Form/FieldLabel/FieldLabel.js +32 -0
- package/dist/components/Form/FieldLabel/FieldLabel.js.map +1 -0
- package/dist/components/Form/FieldLabel/FieldLabel.mjs +30 -0
- package/dist/components/Form/FieldLabel/FieldLabel.mjs.map +1 -0
- package/dist/components/Form/FieldLabel/FieldLabel.types.d.ts +21 -0
- package/dist/components/Form/FieldLabel/FieldLabel.types.d.ts.map +1 -0
- package/dist/components/Form/FieldLabel/index.d.ts +3 -0
- package/dist/components/Form/FieldLabel/index.d.ts.map +1 -0
- package/dist/components/Form/FieldRow/FieldRow.d.ts +15 -0
- package/dist/components/Form/FieldRow/FieldRow.d.ts.map +1 -0
- package/dist/components/Form/FieldRow/FieldRow.js +27 -0
- package/dist/components/Form/FieldRow/FieldRow.js.map +1 -0
- package/dist/components/Form/FieldRow/FieldRow.mjs +25 -0
- package/dist/components/Form/FieldRow/FieldRow.mjs.map +1 -0
- package/dist/components/Form/FieldRow/FieldRow.types.d.ts +27 -0
- package/dist/components/Form/FieldRow/FieldRow.types.d.ts.map +1 -0
- package/dist/components/Form/FieldRow/index.d.ts +3 -0
- package/dist/components/Form/FieldRow/index.d.ts.map +1 -0
- package/dist/components/Form/FormFooter/FormFooter.d.ts +11 -0
- package/dist/components/Form/FormFooter/FormFooter.d.ts.map +1 -0
- package/dist/components/Form/FormFooter/FormFooter.js +20 -0
- package/dist/components/Form/FormFooter/FormFooter.js.map +1 -0
- package/dist/components/Form/FormFooter/FormFooter.mjs +18 -0
- package/dist/components/Form/FormFooter/FormFooter.mjs.map +1 -0
- package/dist/components/Form/FormFooter/FormFooter.types.d.ts +23 -0
- package/dist/components/Form/FormFooter/FormFooter.types.d.ts.map +1 -0
- package/dist/components/Form/FormFooter/index.d.ts +3 -0
- package/dist/components/Form/FormFooter/index.d.ts.map +1 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.d.ts +19 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.d.ts.map +1 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.js +42 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.js.map +1 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.mjs +40 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.mjs.map +1 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.types.d.ts +49 -0
- package/dist/components/Form/FormModalFooter/FormModalFooter.types.d.ts.map +1 -0
- package/dist/components/Form/FormModalFooter/index.d.ts +3 -0
- package/dist/components/Form/FormModalFooter/index.d.ts.map +1 -0
- package/dist/components/Form/FormScrollArea/FormScrollArea.d.ts +14 -0
- package/dist/components/Form/FormScrollArea/FormScrollArea.d.ts.map +1 -0
- package/dist/components/Form/FormScrollArea/FormScrollArea.js +18 -0
- package/dist/components/Form/FormScrollArea/FormScrollArea.js.map +1 -0
- package/dist/components/Form/FormScrollArea/FormScrollArea.mjs +16 -0
- package/dist/components/Form/FormScrollArea/FormScrollArea.mjs.map +1 -0
- package/dist/components/Form/FormScrollArea/index.d.ts +2 -0
- package/dist/components/Form/FormScrollArea/index.d.ts.map +1 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.d.ts +10 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.d.ts.map +1 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.js +43 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.js.map +1 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.mjs +41 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.mjs.map +1 -0
- package/dist/components/Form/FormSection/FlatFormSection.d.ts +6 -0
- package/dist/components/Form/FormSection/FlatFormSection.d.ts.map +1 -0
- package/dist/components/Form/FormSection/FlatFormSection.js +12 -0
- package/dist/components/Form/FormSection/FlatFormSection.js.map +1 -0
- package/dist/components/Form/FormSection/FlatFormSection.mjs +10 -0
- package/dist/components/Form/FormSection/FlatFormSection.mjs.map +1 -0
- package/dist/components/Form/FormSection/FormSection.d.ts +13 -0
- package/dist/components/Form/FormSection/FormSection.d.ts.map +1 -0
- package/dist/components/Form/FormSection/FormSection.js +21 -0
- package/dist/components/Form/FormSection/FormSection.js.map +1 -0
- package/dist/components/Form/FormSection/FormSection.mjs +19 -0
- package/dist/components/Form/FormSection/FormSection.mjs.map +1 -0
- package/dist/components/Form/FormSection/FormSection.types.d.ts +34 -0
- package/dist/components/Form/FormSection/FormSection.types.d.ts.map +1 -0
- package/dist/components/Form/FormSection/index.d.ts +3 -0
- package/dist/components/Form/FormSection/index.d.ts.map +1 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.d.ts +13 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.d.ts.map +1 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.js +30 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.js.map +1 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.mjs +28 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.mjs.map +1 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.types.d.ts +17 -0
- package/dist/components/Form/FormSectionHeader/FormSectionHeader.types.d.ts.map +1 -0
- package/dist/components/Form/FormSectionHeader/index.d.ts +3 -0
- package/dist/components/Form/FormSectionHeader/index.d.ts.map +1 -0
- package/dist/components/Form/index.d.ts +18 -0
- package/dist/components/Form/index.d.ts.map +1 -0
- package/dist/components/Tag/Tag.styles.js +5 -5
- package/dist/components/Tag/Tag.styles.js.map +1 -1
- package/dist/components/Tag/Tag.styles.mjs +5 -5
- package/dist/components/Tag/Tag.styles.mjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.styles.ts +18 -12
- package/src/components/Card/Card.stories.tsx +7 -0
- package/src/components/Card/Card.styles.ts +8 -0
- package/src/components/Card/Card.types.ts +1 -1
- package/src/components/Form/FieldError/FieldError.test.tsx +33 -0
- package/src/components/Form/FieldError/FieldError.tsx +20 -0
- package/src/components/Form/FieldError/index.ts +2 -0
- package/src/components/Form/FieldHelp/FieldHelp.tsx +18 -0
- package/src/components/Form/FieldHelp/index.ts +2 -0
- package/src/components/Form/FieldLabel/FieldLabel.tsx +46 -0
- package/src/components/Form/FieldLabel/FieldLabel.types.ts +21 -0
- package/src/components/Form/FieldLabel/index.ts +2 -0
- package/src/components/Form/FieldRow/FieldRow.stories.tsx +85 -0
- package/src/components/Form/FieldRow/FieldRow.test.tsx +89 -0
- package/src/components/Form/FieldRow/FieldRow.tsx +46 -0
- package/src/components/Form/FieldRow/FieldRow.types.ts +32 -0
- package/src/components/Form/FieldRow/index.ts +2 -0
- package/src/components/Form/FormFooter/FormFooter.stories.tsx +70 -0
- package/src/components/Form/FormFooter/FormFooter.test.tsx +67 -0
- package/src/components/Form/FormFooter/FormFooter.tsx +46 -0
- package/src/components/Form/FormFooter/FormFooter.types.ts +23 -0
- package/src/components/Form/FormFooter/index.ts +2 -0
- package/src/components/Form/FormModalFooter/FormModalFooter.stories.tsx +64 -0
- package/src/components/Form/FormModalFooter/FormModalFooter.test.tsx +155 -0
- package/src/components/Form/FormModalFooter/FormModalFooter.tsx +83 -0
- package/src/components/Form/FormModalFooter/FormModalFooter.types.ts +53 -0
- package/src/components/Form/FormModalFooter/index.ts +2 -0
- package/src/components/Form/FormScrollArea/FormScrollArea.tsx +27 -0
- package/src/components/Form/FormScrollArea/index.ts +1 -0
- package/src/components/Form/FormSection/CollapsibleFormSection.tsx +35 -0
- package/src/components/Form/FormSection/FlatFormSection.tsx +41 -0
- package/src/components/Form/FormSection/FormSection.stories.tsx +115 -0
- package/src/components/Form/FormSection/FormSection.test.tsx +89 -0
- package/src/components/Form/FormSection/FormSection.tsx +33 -0
- package/src/components/Form/FormSection/FormSection.types.ts +34 -0
- package/src/components/Form/FormSection/index.ts +2 -0
- package/src/components/Form/FormSectionHeader/FormSectionHeader.stories.tsx +53 -0
- package/src/components/Form/FormSectionHeader/FormSectionHeader.test.tsx +74 -0
- package/src/components/Form/FormSectionHeader/FormSectionHeader.tsx +48 -0
- package/src/components/Form/FormSectionHeader/FormSectionHeader.types.ts +17 -0
- package/src/components/Form/FormSectionHeader/index.ts +2 -0
- package/src/components/Form/index.ts +17 -0
- package/src/components/Tag/Tag.styles.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -46,11 +46,15 @@ export const buttonColorPaletteStyles: Record<
|
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
soft: {
|
|
49
|
-
|
|
49
|
+
// Same-hue hairline so soft reads as a defined control on white, not a
|
|
50
|
+
// floating fill. `lighter` sits well below `outline`'s `.main` border, so
|
|
51
|
+
// soft stays the quieter of the two bordered variants; it also equals the
|
|
52
|
+
// hover/active fill, so the border melts into the fill on interaction.
|
|
53
|
+
borderColor: 'primary.lighter',
|
|
50
54
|
bgColor: 'primary.extralight', // #E8EEFB
|
|
51
55
|
color: 'primary.dark', // #0D317D
|
|
52
56
|
_hover: {
|
|
53
|
-
borderColor: '
|
|
57
|
+
borderColor: 'primary.lighter',
|
|
54
58
|
bgColor: 'primary.lighter', // #B9CBF3
|
|
55
59
|
},
|
|
56
60
|
_active: {
|
|
@@ -102,11 +106,11 @@ export const buttonColorPaletteStyles: Record<
|
|
|
102
106
|
},
|
|
103
107
|
},
|
|
104
108
|
soft: {
|
|
105
|
-
borderColor: '
|
|
109
|
+
borderColor: 'secondary.lighter',
|
|
106
110
|
bgColor: 'secondary.extralight', // #F4E8FB
|
|
107
111
|
color: 'secondary.dark', // #570D7D
|
|
108
112
|
_hover: {
|
|
109
|
-
borderColor: '
|
|
113
|
+
borderColor: 'secondary.lighter',
|
|
110
114
|
bgColor: 'secondary.lighter', // #DEB9F3
|
|
111
115
|
},
|
|
112
116
|
_active: {
|
|
@@ -158,11 +162,11 @@ export const buttonColorPaletteStyles: Record<
|
|
|
158
162
|
},
|
|
159
163
|
},
|
|
160
164
|
soft: {
|
|
161
|
-
borderColor: '
|
|
165
|
+
borderColor: 'danger.lighter',
|
|
162
166
|
bgColor: 'danger.extralight', // #FBE8E9
|
|
163
167
|
color: 'danger.dark', // #7D0D14
|
|
164
168
|
_hover: {
|
|
165
|
-
borderColor: '
|
|
169
|
+
borderColor: 'danger.lighter',
|
|
166
170
|
bgColor: 'danger.lighter', // #F3B9BD
|
|
167
171
|
},
|
|
168
172
|
_active: {
|
|
@@ -214,11 +218,11 @@ export const buttonColorPaletteStyles: Record<
|
|
|
214
218
|
},
|
|
215
219
|
},
|
|
216
220
|
soft: {
|
|
217
|
-
borderColor: '
|
|
221
|
+
borderColor: 'success.lighter',
|
|
218
222
|
bgColor: 'success.extralight', // #E9FBE8
|
|
219
223
|
color: 'success.dark', // #147D0D
|
|
220
224
|
_hover: {
|
|
221
|
-
borderColor: '
|
|
225
|
+
borderColor: 'success.lighter',
|
|
222
226
|
bgColor: 'success.lighter', // #BDF3B9
|
|
223
227
|
},
|
|
224
228
|
_active: {
|
|
@@ -273,11 +277,11 @@ export const buttonColorPaletteStyles: Record<
|
|
|
273
277
|
},
|
|
274
278
|
},
|
|
275
279
|
soft: {
|
|
276
|
-
borderColor: '
|
|
280
|
+
borderColor: 'warning.lighter',
|
|
277
281
|
bgColor: 'warning.extralight', // #FBF6E8
|
|
278
282
|
color: 'warning.dark', // #7D610D
|
|
279
283
|
_hover: {
|
|
280
|
-
borderColor: '
|
|
284
|
+
borderColor: 'warning.lighter',
|
|
281
285
|
bgColor: 'warning.lighter', // #F3E4B9
|
|
282
286
|
},
|
|
283
287
|
_active: {
|
|
@@ -329,14 +333,16 @@ export const buttonColorPaletteStyles: Record<
|
|
|
329
333
|
},
|
|
330
334
|
},
|
|
331
335
|
soft: {
|
|
332
|
-
|
|
336
|
+
// `border.default` (not a gray tint) so the neutral hairline stays visible
|
|
337
|
+
// in dark mode, where `border.subtle` collapses into the fill.
|
|
338
|
+
borderColor: 'border.default', // gray.300 / gray.1100 (_dark)
|
|
333
339
|
// Neutral fills flip with the mode (light values preserved: bg.subtle →
|
|
334
340
|
// gray.50, bg.muted → gray.100). Without this the surface stayed light in
|
|
335
341
|
// dark mode while the fg.default text flipped to light = invisible.
|
|
336
342
|
bgColor: 'bg.subtle', // gray.50 / gray.1300 (_dark)
|
|
337
343
|
color: 'fg.default',
|
|
338
344
|
_hover: {
|
|
339
|
-
borderColor: '
|
|
345
|
+
borderColor: 'border.default',
|
|
340
346
|
bgColor: 'bg.muted', // gray.100 / gray.1200 (_dark)
|
|
341
347
|
},
|
|
342
348
|
_active: {
|
|
@@ -16,4 +16,12 @@ export const variantStyles = {
|
|
|
16
16
|
border: '1px solid',
|
|
17
17
|
borderColor: 'primary.light',
|
|
18
18
|
},
|
|
19
|
+
// Soft resting elevation — a gentle shadow does the separating, so the border
|
|
20
|
+
// can soften to `border.subtle` (which alone would be too faint, especially in
|
|
21
|
+
// dark mode). Use for standalone content cards that should read as raised
|
|
22
|
+
// objects rather than flat framed boxes.
|
|
23
|
+
elevated: {
|
|
24
|
+
boxShadow: 'sm',
|
|
25
|
+
borderColor: 'border.subtle',
|
|
26
|
+
},
|
|
19
27
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { FieldError } from './FieldError';
|
|
6
|
+
|
|
7
|
+
// Field.ErrorText needs a Field.Root + invalid context to render; stub it to a
|
|
8
|
+
// passthrough so the test isolates FieldError's own null-when-empty guard.
|
|
9
|
+
vi.mock('@chakra-ui/react', () => ({
|
|
10
|
+
Field: {
|
|
11
|
+
ErrorText: ({ children }: { children?: ReactNode }) => (
|
|
12
|
+
<span data-testid="error-text">{children}</span>
|
|
13
|
+
),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('FieldError', () => {
|
|
18
|
+
it('renders nothing when empty (safe to mount unconditionally)', () => {
|
|
19
|
+
const { rerender } = render(<FieldError />);
|
|
20
|
+
expect(screen.queryByTestId('error-text')).toBeNull();
|
|
21
|
+
|
|
22
|
+
rerender(<FieldError>{''}</FieldError>);
|
|
23
|
+
expect(screen.queryByTestId('error-text')).toBeNull();
|
|
24
|
+
|
|
25
|
+
rerender(<FieldError>{false}</FieldError>);
|
|
26
|
+
expect(screen.queryByTestId('error-text')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders the message when present', () => {
|
|
30
|
+
render(<FieldError>Display name is required.</FieldError>);
|
|
31
|
+
expect(screen.getByText('Display name is required.')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Field } from '@chakra-ui/react';
|
|
3
|
+
|
|
4
|
+
export interface FieldErrorProps {
|
|
5
|
+
/** Error message. Renders nothing when empty, so it is safe to mount always. */
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Field-level error message. Renders through Chakra's `Field.ErrorText`, so it
|
|
11
|
+
* shows only when the enclosing `FormControl`/`Field.Root` is `invalid`, and is
|
|
12
|
+
* wired to the control via `aria-describedby` + `aria-invalid` automatically.
|
|
13
|
+
* Use it inside a `FieldRow` (which flips the field to invalid when an `error`
|
|
14
|
+
* is set) — it replaces the hand-rolled `<Subtext color="danger.main">` error
|
|
15
|
+
* rows scattered across the app.
|
|
16
|
+
*/
|
|
17
|
+
export const FieldError = ({ children }: FieldErrorProps) => {
|
|
18
|
+
if (children == null || children === false || children === '') return null;
|
|
19
|
+
return <Field.ErrorText color="danger.main">{children}</Field.ErrorText>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Field } from '@chakra-ui/react';
|
|
3
|
+
|
|
4
|
+
export interface FieldHelpProps {
|
|
5
|
+
/** Helper text rendered under the control. */
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inline helper text under a form control. Renders through Chakra's
|
|
11
|
+
* `Field.HelperText`, so it is associated with the control via
|
|
12
|
+
* `aria-describedby` when used inside a `FieldRow`/`FormControl`. For a hover
|
|
13
|
+
* info-popover instead of always-visible text, pass a logician `InfoSprinkle`
|
|
14
|
+
* into the `FieldRow` `endAdornment` slot.
|
|
15
|
+
*/
|
|
16
|
+
export const FieldHelp = ({ children }: FieldHelpProps) => {
|
|
17
|
+
return <Field.HelperText>{children}</Field.HelperText>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Box, Flex } from '@chakra-ui/react';
|
|
2
|
+
|
|
3
|
+
import { FormLabel } from '@/components/FormLabel';
|
|
4
|
+
import { Subtext } from '@/components/Typography';
|
|
5
|
+
|
|
6
|
+
import { FieldLabelProps } from './FieldLabel.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Label row for a form field: a semibold heading, an optional description, and
|
|
10
|
+
* an `endAdornment` slot on the right (count badge, action button, info-sprinkle
|
|
11
|
+
* trigger). The required asterisk is supplied automatically by the enclosing
|
|
12
|
+
* `Field` context when the field is `required` (see `FieldRow`).
|
|
13
|
+
*
|
|
14
|
+
* Presentational — pair it with `FieldRow` (or any logician `FormControl`) so
|
|
15
|
+
* the heading is programmatically associated with its control.
|
|
16
|
+
*/
|
|
17
|
+
export const FieldLabel = ({
|
|
18
|
+
heading,
|
|
19
|
+
description,
|
|
20
|
+
endAdornment,
|
|
21
|
+
mb = 0,
|
|
22
|
+
htmlFor,
|
|
23
|
+
}: FieldLabelProps) => {
|
|
24
|
+
return (
|
|
25
|
+
<Flex align="center" justify="space-between" gap={2} mb={mb} w="100%">
|
|
26
|
+
<Box>
|
|
27
|
+
{heading != null && (
|
|
28
|
+
<FormLabel
|
|
29
|
+
mb={0}
|
|
30
|
+
fontWeight="semibold"
|
|
31
|
+
color="fg.default"
|
|
32
|
+
htmlFor={htmlFor}
|
|
33
|
+
>
|
|
34
|
+
{heading}
|
|
35
|
+
</FormLabel>
|
|
36
|
+
)}
|
|
37
|
+
{description && (
|
|
38
|
+
<Subtext fontWeight="normal" color="fg.muted">
|
|
39
|
+
{description}
|
|
40
|
+
</Subtext>
|
|
41
|
+
)}
|
|
42
|
+
</Box>
|
|
43
|
+
{endAdornment}
|
|
44
|
+
</Flex>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FieldLabelProps {
|
|
4
|
+
/** Label heading. */
|
|
5
|
+
heading: ReactNode;
|
|
6
|
+
/** Secondary description rendered under the heading. */
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Inline element rendered to the right of the heading row (e.g. button, count
|
|
10
|
+
* badge, info-sprinkle popover trigger).
|
|
11
|
+
*/
|
|
12
|
+
endAdornment?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Associates the label with a control by id. Normally supplied by the
|
|
15
|
+
* enclosing `Field` context (via `FieldRow`/`FormControl`); pass it explicitly
|
|
16
|
+
* only when using `FieldLabel` outside that context.
|
|
17
|
+
*/
|
|
18
|
+
htmlFor?: string;
|
|
19
|
+
/** Bottom margin in spacing units. */
|
|
20
|
+
mb?: number;
|
|
21
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { Input } from '@/components/Input';
|
|
4
|
+
import { SelectField } from '@/components/Select';
|
|
5
|
+
import { Textarea } from '@/components/Textarea';
|
|
6
|
+
|
|
7
|
+
import { FieldRow } from './FieldRow';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof FieldRow> = {
|
|
10
|
+
title: 'Components/Form/FieldRow',
|
|
11
|
+
component: FieldRow,
|
|
12
|
+
args: {
|
|
13
|
+
label: 'Display name',
|
|
14
|
+
children: <Input placeholder="Enter a name" />,
|
|
15
|
+
},
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'The composition keystone for a form field. Wraps a control in a ' +
|
|
21
|
+
'logician `FormControl` and lays out label, description, helper and ' +
|
|
22
|
+
'error consistently, delegating all label↔control / aria wiring to ' +
|
|
23
|
+
'the Chakra `Field` context.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof FieldRow>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {};
|
|
33
|
+
|
|
34
|
+
export const Required: Story = {
|
|
35
|
+
args: { required: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithDescription: Story = {
|
|
39
|
+
args: { description: 'Shown to teammates who can see this chatbot.' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const WithHelperText: Story = {
|
|
43
|
+
args: { helperText: 'Up to 40 characters.' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const WithError: Story = {
|
|
47
|
+
args: { required: true, error: 'Display name is required.' },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithEndAdornment: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
label: 'Recommended questions',
|
|
53
|
+
endAdornment: '2 / 5',
|
|
54
|
+
children: <Input placeholder="Add a question" />,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Disabled: Story = {
|
|
59
|
+
args: { disabled: true },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const TextareaControl: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
label: 'System instructions',
|
|
65
|
+
helperText: 'Describe how the assistant should behave.',
|
|
66
|
+
children: <Textarea placeholder="You are a helpful assistant…" />,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const SelectControl: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
label: 'Aspect ratio',
|
|
73
|
+
children: (
|
|
74
|
+
<SelectField
|
|
75
|
+
options={[
|
|
76
|
+
{ label: '16:9', value: '16:9' },
|
|
77
|
+
{ label: '9:16', value: '9:16' },
|
|
78
|
+
{ label: '1:1', value: '1:1' },
|
|
79
|
+
]}
|
|
80
|
+
value="16:9"
|
|
81
|
+
onChange={() => {}}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { FieldRow } from './FieldRow';
|
|
6
|
+
|
|
7
|
+
// Expose the `invalid` value FieldRow computes onto the DOM so the derivation
|
|
8
|
+
// (error -> invalid, with an explicit `invalid` override winning) is assertable
|
|
9
|
+
// without a full Chakra Field/provider render. FieldRow now composes its label
|
|
10
|
+
// out of the design system's own FormControl/FormLabel/Typography modules, so
|
|
11
|
+
// mock those directly.
|
|
12
|
+
vi.mock('@/components/FormControl', () => ({
|
|
13
|
+
FormControl: ({
|
|
14
|
+
children,
|
|
15
|
+
invalid,
|
|
16
|
+
}: {
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
invalid?: boolean;
|
|
19
|
+
}) => <div data-invalid={String(Boolean(invalid))}>{children}</div>,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('@/components/FormLabel', () => ({
|
|
23
|
+
FormLabel: ({ children }: { children?: ReactNode }) => (
|
|
24
|
+
<label>{children}</label>
|
|
25
|
+
),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('@/components/Typography', () => ({
|
|
29
|
+
Subtext: ({ children }: { children?: ReactNode }) => <span>{children}</span>,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@chakra-ui/react', () => ({
|
|
33
|
+
Box: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
|
34
|
+
Flex: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
|
35
|
+
Field: {
|
|
36
|
+
HelperText: ({ children }: { children?: ReactNode }) => (
|
|
37
|
+
<p data-testid="helper">{children}</p>
|
|
38
|
+
),
|
|
39
|
+
ErrorText: ({ children }: { children?: ReactNode }) => (
|
|
40
|
+
<p data-testid="error">{children}</p>
|
|
41
|
+
),
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const control = <input aria-label="name" />;
|
|
46
|
+
|
|
47
|
+
describe('FieldRow', () => {
|
|
48
|
+
it('renders label, control, helper and error slots', () => {
|
|
49
|
+
render(
|
|
50
|
+
<FieldRow label="Display name" helperText="Up to 40 chars" error="Nope">
|
|
51
|
+
{control}
|
|
52
|
+
</FieldRow>
|
|
53
|
+
);
|
|
54
|
+
expect(screen.getByText('Display name')).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByLabelText('name')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByTestId('helper')).toHaveTextContent('Up to 40 chars');
|
|
57
|
+
expect(screen.getByTestId('error')).toHaveTextContent('Nope');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('flips the field to invalid when an error is set', () => {
|
|
61
|
+
const { container } = render(
|
|
62
|
+
<FieldRow label="Name" error="Required">
|
|
63
|
+
{control}
|
|
64
|
+
</FieldRow>
|
|
65
|
+
);
|
|
66
|
+
expect(container.querySelector('[data-invalid="true"]')).not.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('is not invalid without an error', () => {
|
|
70
|
+
const { container } = render(<FieldRow label="Name">{control}</FieldRow>);
|
|
71
|
+
expect(container.querySelector('[data-invalid="false"]')).not.toBeNull();
|
|
72
|
+
expect(screen.queryByTestId('error')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('lets an explicit invalid prop win over the error-derived value', () => {
|
|
76
|
+
const { container } = render(
|
|
77
|
+
<FieldRow label="Name" error="Required" invalid={false}>
|
|
78
|
+
{control}
|
|
79
|
+
</FieldRow>
|
|
80
|
+
);
|
|
81
|
+
expect(container.querySelector('[data-invalid="false"]')).not.toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('omits the label row when there is no label or adornment', () => {
|
|
85
|
+
const { container } = render(<FieldRow>{control}</FieldRow>);
|
|
86
|
+
expect(container.querySelector('label')).toBeNull();
|
|
87
|
+
expect(screen.getByLabelText('name')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { FormControl } from '@/components/FormControl';
|
|
2
|
+
|
|
3
|
+
import { FieldError } from '../FieldError';
|
|
4
|
+
import { FieldHelp } from '../FieldHelp';
|
|
5
|
+
import { FieldLabel } from '../FieldLabel';
|
|
6
|
+
import { FieldRowProps } from './FieldRow.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The composition keystone for a form field: wraps a control in a logician
|
|
10
|
+
* `FormControl` (Chakra `Field.Root`) and lays out its label, optional
|
|
11
|
+
* description/help, and error consistently. Because it delegates to the `Field`
|
|
12
|
+
* context, the label↔control association, the `required` asterisk, `aria-invalid`
|
|
13
|
+
* and the error/helper `aria-describedby` wiring are all handled automatically —
|
|
14
|
+
* callers never thread ids by hand.
|
|
15
|
+
*
|
|
16
|
+
* It is form-library agnostic: pass `value`/`onChange` controls directly (media
|
|
17
|
+
* forms) or render a form-connected adapter as the child (studio/admin forms). Set
|
|
18
|
+
* `error` to flip the field to invalid and surface the message.
|
|
19
|
+
*/
|
|
20
|
+
export const FieldRow = ({
|
|
21
|
+
label,
|
|
22
|
+
description,
|
|
23
|
+
endAdornment,
|
|
24
|
+
helperText,
|
|
25
|
+
error,
|
|
26
|
+
invalid,
|
|
27
|
+
children,
|
|
28
|
+
labelProps,
|
|
29
|
+
...rootProps
|
|
30
|
+
}: FieldRowProps) => {
|
|
31
|
+
return (
|
|
32
|
+
<FormControl invalid={invalid ?? Boolean(error)} {...rootProps}>
|
|
33
|
+
{(label != null || endAdornment != null) && (
|
|
34
|
+
<FieldLabel
|
|
35
|
+
heading={label}
|
|
36
|
+
description={description}
|
|
37
|
+
endAdornment={endAdornment}
|
|
38
|
+
{...labelProps}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
{children}
|
|
42
|
+
{helperText != null && <FieldHelp>{helperText}</FieldHelp>}
|
|
43
|
+
<FieldError>{error}</FieldError>
|
|
44
|
+
</FormControl>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { FormControlProps } from '@/components/FormControl';
|
|
4
|
+
|
|
5
|
+
import { FieldLabelProps } from '../FieldLabel';
|
|
6
|
+
|
|
7
|
+
export interface FieldRowProps extends Omit<FormControlProps, 'label'> {
|
|
8
|
+
/** Field label heading. Omit for an unlabeled control row. */
|
|
9
|
+
label?: ReactNode;
|
|
10
|
+
/** Secondary description rendered under the label. */
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Inline node at the right of the label row — a count badge, an action
|
|
14
|
+
* button, or a logician `InfoSprinkle` help popover.
|
|
15
|
+
*/
|
|
16
|
+
endAdornment?: ReactNode;
|
|
17
|
+
/** Always-visible helper text rendered under the control. */
|
|
18
|
+
helperText?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Error message. When set (and `invalid` is not explicitly overridden) the
|
|
21
|
+
* field renders as invalid, so the control gets `aria-invalid` and the error
|
|
22
|
+
* is announced and associated via `aria-describedby`.
|
|
23
|
+
*/
|
|
24
|
+
error?: ReactNode;
|
|
25
|
+
/** The control (input/select/switch/…) this row wraps. */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
/** Extra props forwarded to the inner `FieldLabel` (e.g. custom `mb`). */
|
|
28
|
+
labelProps?: Omit<
|
|
29
|
+
FieldLabelProps,
|
|
30
|
+
'heading' | 'description' | 'endAdornment'
|
|
31
|
+
>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Box } from '@chakra-ui/react';
|
|
2
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import { Button } from '@/components/Button';
|
|
5
|
+
import { Subtitle } from '@/components/Typography';
|
|
6
|
+
|
|
7
|
+
import { FormFooter } from './FormFooter';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof FormFooter> = {
|
|
10
|
+
title: 'Components/Form/FormFooter',
|
|
11
|
+
component: FormFooter,
|
|
12
|
+
decorators: [
|
|
13
|
+
(Story) => (
|
|
14
|
+
<Box w="420px" bg="bg.canvas">
|
|
15
|
+
<Story />
|
|
16
|
+
</Box>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
parameters: {
|
|
20
|
+
docs: {
|
|
21
|
+
description: {
|
|
22
|
+
component:
|
|
23
|
+
'The sticky footer shell shared by the studio settings footer and ' +
|
|
24
|
+
'the image/video generation footers: an optional left `start` slot ' +
|
|
25
|
+
'against a right group of `meta` + `action`.',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof FormFooter>;
|
|
33
|
+
|
|
34
|
+
/** Studio settings footer shape: destructive start + saved-at meta + save action. */
|
|
35
|
+
export const WithStartMetaAndAction: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
start: (
|
|
38
|
+
<Button size="sm" variant="ghost" colorPalette="danger">
|
|
39
|
+
Delete
|
|
40
|
+
</Button>
|
|
41
|
+
),
|
|
42
|
+
meta: <Subtitle>Updated 2 minutes ago</Subtitle>,
|
|
43
|
+
action: (
|
|
44
|
+
<Button size="sm" variant="solid">
|
|
45
|
+
Save
|
|
46
|
+
</Button>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Media generation footer shape: a single full-width action. */
|
|
52
|
+
export const FullWidthAction: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
action: (
|
|
55
|
+
<Button variant="solid" size="lg" w="100%">
|
|
56
|
+
Generate image · 12
|
|
57
|
+
</Button>
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const ActionOnly: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
action: (
|
|
65
|
+
<Button size="sm" variant="solid">
|
|
66
|
+
Save
|
|
67
|
+
</Button>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { FormFooter } from './FormFooter';
|
|
6
|
+
|
|
7
|
+
// Surface the layout-driving props (justify / position) onto the DOM so the
|
|
8
|
+
// slot layout and sticky behavior are assertable without a Chakra provider.
|
|
9
|
+
vi.mock('@chakra-ui/react', () => ({
|
|
10
|
+
Flex: ({
|
|
11
|
+
children,
|
|
12
|
+
justify,
|
|
13
|
+
position,
|
|
14
|
+
}: {
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
justify?: string;
|
|
17
|
+
position?: string;
|
|
18
|
+
}) => (
|
|
19
|
+
<div data-justify={justify ?? ''} data-position={position ?? ''}>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const outer = (container: HTMLElement) => container.firstChild as HTMLElement;
|
|
26
|
+
|
|
27
|
+
describe('FormFooter', () => {
|
|
28
|
+
it('renders start, meta and action slots', () => {
|
|
29
|
+
render(
|
|
30
|
+
<FormFooter
|
|
31
|
+
start={<button>Delete</button>}
|
|
32
|
+
meta={<span>Updated now</span>}
|
|
33
|
+
action={<button>Save</button>}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
expect(screen.getByText('Delete')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByText('Updated now')).toBeInTheDocument();
|
|
38
|
+
expect(screen.getByText('Save')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('justifies space-between when a start slot is present', () => {
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<FormFooter
|
|
44
|
+
start={<button>Delete</button>}
|
|
45
|
+
action={<button>Save</button>}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
expect(outer(container)).toHaveAttribute('data-justify', 'space-between');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('justifies flex-end when there is no start slot (media full-width action)', () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<FormFooter action={<button>Generate</button>} />
|
|
54
|
+
);
|
|
55
|
+
expect(outer(container)).toHaveAttribute('data-justify', 'flex-end');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('is sticky by default and drops sticky when disabled', () => {
|
|
59
|
+
const { container, rerender } = render(
|
|
60
|
+
<FormFooter action={<button>Save</button>} />
|
|
61
|
+
);
|
|
62
|
+
expect(outer(container)).toHaveAttribute('data-position', 'sticky');
|
|
63
|
+
|
|
64
|
+
rerender(<FormFooter action={<button>Save</button>} sticky={false} />);
|
|
65
|
+
expect(outer(container)).toHaveAttribute('data-position', '');
|
|
66
|
+
});
|
|
67
|
+
});
|