@mindlogic-ai/logician-ui 4.0.0-alpha.13 → 4.0.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/components/Button/Button.styles.d.ts.map +1 -1
  2. package/dist/components/Button/Button.styles.js +17 -12
  3. package/dist/components/Button/Button.styles.js.map +1 -1
  4. package/dist/components/Button/Button.styles.mjs +17 -12
  5. package/dist/components/Button/Button.styles.mjs.map +1 -1
  6. package/dist/components/Card/Card.styles.d.ts +4 -0
  7. package/dist/components/Card/Card.styles.d.ts.map +1 -1
  8. package/dist/components/Card/Card.styles.js +8 -0
  9. package/dist/components/Card/Card.styles.js.map +1 -1
  10. package/dist/components/Card/Card.styles.mjs +8 -0
  11. package/dist/components/Card/Card.styles.mjs.map +1 -1
  12. package/dist/components/Card/Card.types.d.ts +1 -1
  13. package/dist/components/Card/Card.types.d.ts.map +1 -1
  14. package/dist/components/Form/FieldError/FieldError.d.ts +15 -0
  15. package/dist/components/Form/FieldError/FieldError.d.ts.map +1 -0
  16. package/dist/components/Form/FieldError/FieldError.js +22 -0
  17. package/dist/components/Form/FieldError/FieldError.js.map +1 -0
  18. package/dist/components/Form/FieldError/FieldError.mjs +20 -0
  19. package/dist/components/Form/FieldError/FieldError.mjs.map +1 -0
  20. package/dist/components/Form/FieldError/index.d.ts +3 -0
  21. package/dist/components/Form/FieldError/index.d.ts.map +1 -0
  22. package/dist/components/Form/FieldHelp/FieldHelp.d.ts +14 -0
  23. package/dist/components/Form/FieldHelp/FieldHelp.d.ts.map +1 -0
  24. package/dist/components/Form/FieldHelp/FieldHelp.js +19 -0
  25. package/dist/components/Form/FieldHelp/FieldHelp.js.map +1 -0
  26. package/dist/components/Form/FieldHelp/FieldHelp.mjs +17 -0
  27. package/dist/components/Form/FieldHelp/FieldHelp.mjs.map +1 -0
  28. package/dist/components/Form/FieldHelp/index.d.ts +3 -0
  29. package/dist/components/Form/FieldHelp/index.d.ts.map +1 -0
  30. package/dist/components/Form/FieldLabel/FieldLabel.d.ts +12 -0
  31. package/dist/components/Form/FieldLabel/FieldLabel.d.ts.map +1 -0
  32. package/dist/components/Form/FieldLabel/FieldLabel.js +32 -0
  33. package/dist/components/Form/FieldLabel/FieldLabel.js.map +1 -0
  34. package/dist/components/Form/FieldLabel/FieldLabel.mjs +30 -0
  35. package/dist/components/Form/FieldLabel/FieldLabel.mjs.map +1 -0
  36. package/dist/components/Form/FieldLabel/FieldLabel.types.d.ts +21 -0
  37. package/dist/components/Form/FieldLabel/FieldLabel.types.d.ts.map +1 -0
  38. package/dist/components/Form/FieldLabel/index.d.ts +3 -0
  39. package/dist/components/Form/FieldLabel/index.d.ts.map +1 -0
  40. package/dist/components/Form/FieldRow/FieldRow.d.ts +15 -0
  41. package/dist/components/Form/FieldRow/FieldRow.d.ts.map +1 -0
  42. package/dist/components/Form/FieldRow/FieldRow.js +27 -0
  43. package/dist/components/Form/FieldRow/FieldRow.js.map +1 -0
  44. package/dist/components/Form/FieldRow/FieldRow.mjs +25 -0
  45. package/dist/components/Form/FieldRow/FieldRow.mjs.map +1 -0
  46. package/dist/components/Form/FieldRow/FieldRow.types.d.ts +27 -0
  47. package/dist/components/Form/FieldRow/FieldRow.types.d.ts.map +1 -0
  48. package/dist/components/Form/FieldRow/index.d.ts +3 -0
  49. package/dist/components/Form/FieldRow/index.d.ts.map +1 -0
  50. package/dist/components/Form/FormFooter/FormFooter.d.ts +11 -0
  51. package/dist/components/Form/FormFooter/FormFooter.d.ts.map +1 -0
  52. package/dist/components/Form/FormFooter/FormFooter.js +20 -0
  53. package/dist/components/Form/FormFooter/FormFooter.js.map +1 -0
  54. package/dist/components/Form/FormFooter/FormFooter.mjs +18 -0
  55. package/dist/components/Form/FormFooter/FormFooter.mjs.map +1 -0
  56. package/dist/components/Form/FormFooter/FormFooter.types.d.ts +23 -0
  57. package/dist/components/Form/FormFooter/FormFooter.types.d.ts.map +1 -0
  58. package/dist/components/Form/FormFooter/index.d.ts +3 -0
  59. package/dist/components/Form/FormFooter/index.d.ts.map +1 -0
  60. package/dist/components/Form/FormModalFooter/FormModalFooter.d.ts +19 -0
  61. package/dist/components/Form/FormModalFooter/FormModalFooter.d.ts.map +1 -0
  62. package/dist/components/Form/FormModalFooter/FormModalFooter.js +42 -0
  63. package/dist/components/Form/FormModalFooter/FormModalFooter.js.map +1 -0
  64. package/dist/components/Form/FormModalFooter/FormModalFooter.mjs +40 -0
  65. package/dist/components/Form/FormModalFooter/FormModalFooter.mjs.map +1 -0
  66. package/dist/components/Form/FormModalFooter/FormModalFooter.types.d.ts +49 -0
  67. package/dist/components/Form/FormModalFooter/FormModalFooter.types.d.ts.map +1 -0
  68. package/dist/components/Form/FormModalFooter/index.d.ts +3 -0
  69. package/dist/components/Form/FormModalFooter/index.d.ts.map +1 -0
  70. package/dist/components/Form/FormScrollArea/FormScrollArea.d.ts +14 -0
  71. package/dist/components/Form/FormScrollArea/FormScrollArea.d.ts.map +1 -0
  72. package/dist/components/Form/FormScrollArea/FormScrollArea.js +18 -0
  73. package/dist/components/Form/FormScrollArea/FormScrollArea.js.map +1 -0
  74. package/dist/components/Form/FormScrollArea/FormScrollArea.mjs +16 -0
  75. package/dist/components/Form/FormScrollArea/FormScrollArea.mjs.map +1 -0
  76. package/dist/components/Form/FormScrollArea/index.d.ts +2 -0
  77. package/dist/components/Form/FormScrollArea/index.d.ts.map +1 -0
  78. package/dist/components/Form/FormSection/CollapsibleFormSection.d.ts +10 -0
  79. package/dist/components/Form/FormSection/CollapsibleFormSection.d.ts.map +1 -0
  80. package/dist/components/Form/FormSection/CollapsibleFormSection.js +43 -0
  81. package/dist/components/Form/FormSection/CollapsibleFormSection.js.map +1 -0
  82. package/dist/components/Form/FormSection/CollapsibleFormSection.mjs +41 -0
  83. package/dist/components/Form/FormSection/CollapsibleFormSection.mjs.map +1 -0
  84. package/dist/components/Form/FormSection/FlatFormSection.d.ts +6 -0
  85. package/dist/components/Form/FormSection/FlatFormSection.d.ts.map +1 -0
  86. package/dist/components/Form/FormSection/FlatFormSection.js +12 -0
  87. package/dist/components/Form/FormSection/FlatFormSection.js.map +1 -0
  88. package/dist/components/Form/FormSection/FlatFormSection.mjs +10 -0
  89. package/dist/components/Form/FormSection/FlatFormSection.mjs.map +1 -0
  90. package/dist/components/Form/FormSection/FormSection.d.ts +13 -0
  91. package/dist/components/Form/FormSection/FormSection.d.ts.map +1 -0
  92. package/dist/components/Form/FormSection/FormSection.js +21 -0
  93. package/dist/components/Form/FormSection/FormSection.js.map +1 -0
  94. package/dist/components/Form/FormSection/FormSection.mjs +19 -0
  95. package/dist/components/Form/FormSection/FormSection.mjs.map +1 -0
  96. package/dist/components/Form/FormSection/FormSection.types.d.ts +34 -0
  97. package/dist/components/Form/FormSection/FormSection.types.d.ts.map +1 -0
  98. package/dist/components/Form/FormSection/index.d.ts +3 -0
  99. package/dist/components/Form/FormSection/index.d.ts.map +1 -0
  100. package/dist/components/Form/FormSectionHeader/FormSectionHeader.d.ts +13 -0
  101. package/dist/components/Form/FormSectionHeader/FormSectionHeader.d.ts.map +1 -0
  102. package/dist/components/Form/FormSectionHeader/FormSectionHeader.js +30 -0
  103. package/dist/components/Form/FormSectionHeader/FormSectionHeader.js.map +1 -0
  104. package/dist/components/Form/FormSectionHeader/FormSectionHeader.mjs +28 -0
  105. package/dist/components/Form/FormSectionHeader/FormSectionHeader.mjs.map +1 -0
  106. package/dist/components/Form/FormSectionHeader/FormSectionHeader.types.d.ts +17 -0
  107. package/dist/components/Form/FormSectionHeader/FormSectionHeader.types.d.ts.map +1 -0
  108. package/dist/components/Form/FormSectionHeader/index.d.ts +3 -0
  109. package/dist/components/Form/FormSectionHeader/index.d.ts.map +1 -0
  110. package/dist/components/Form/index.d.ts +18 -0
  111. package/dist/components/Form/index.d.ts.map +1 -0
  112. package/dist/index.d.ts +1 -0
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +18 -0
  115. package/dist/index.js.map +1 -1
  116. package/dist/index.mjs +9 -0
  117. package/dist/index.mjs.map +1 -1
  118. package/package.json +1 -1
  119. package/src/components/Button/Button.styles.ts +17 -12
  120. package/src/components/Card/Card.stories.tsx +7 -0
  121. package/src/components/Card/Card.styles.ts +8 -0
  122. package/src/components/Card/Card.types.ts +1 -1
  123. package/src/components/Form/FieldError/FieldError.test.tsx +33 -0
  124. package/src/components/Form/FieldError/FieldError.tsx +20 -0
  125. package/src/components/Form/FieldError/index.ts +2 -0
  126. package/src/components/Form/FieldHelp/FieldHelp.tsx +18 -0
  127. package/src/components/Form/FieldHelp/index.ts +2 -0
  128. package/src/components/Form/FieldLabel/FieldLabel.tsx +46 -0
  129. package/src/components/Form/FieldLabel/FieldLabel.types.ts +21 -0
  130. package/src/components/Form/FieldLabel/index.ts +2 -0
  131. package/src/components/Form/FieldRow/FieldRow.stories.tsx +85 -0
  132. package/src/components/Form/FieldRow/FieldRow.test.tsx +89 -0
  133. package/src/components/Form/FieldRow/FieldRow.tsx +46 -0
  134. package/src/components/Form/FieldRow/FieldRow.types.ts +32 -0
  135. package/src/components/Form/FieldRow/index.ts +2 -0
  136. package/src/components/Form/FormFooter/FormFooter.stories.tsx +70 -0
  137. package/src/components/Form/FormFooter/FormFooter.test.tsx +67 -0
  138. package/src/components/Form/FormFooter/FormFooter.tsx +46 -0
  139. package/src/components/Form/FormFooter/FormFooter.types.ts +23 -0
  140. package/src/components/Form/FormFooter/index.ts +2 -0
  141. package/src/components/Form/FormModalFooter/FormModalFooter.stories.tsx +64 -0
  142. package/src/components/Form/FormModalFooter/FormModalFooter.test.tsx +155 -0
  143. package/src/components/Form/FormModalFooter/FormModalFooter.tsx +83 -0
  144. package/src/components/Form/FormModalFooter/FormModalFooter.types.ts +53 -0
  145. package/src/components/Form/FormModalFooter/index.ts +2 -0
  146. package/src/components/Form/FormScrollArea/FormScrollArea.tsx +27 -0
  147. package/src/components/Form/FormScrollArea/index.ts +1 -0
  148. package/src/components/Form/FormSection/CollapsibleFormSection.tsx +35 -0
  149. package/src/components/Form/FormSection/FlatFormSection.tsx +41 -0
  150. package/src/components/Form/FormSection/FormSection.stories.tsx +115 -0
  151. package/src/components/Form/FormSection/FormSection.test.tsx +89 -0
  152. package/src/components/Form/FormSection/FormSection.tsx +33 -0
  153. package/src/components/Form/FormSection/FormSection.types.ts +34 -0
  154. package/src/components/Form/FormSection/index.ts +2 -0
  155. package/src/components/Form/FormSectionHeader/FormSectionHeader.stories.tsx +53 -0
  156. package/src/components/Form/FormSectionHeader/FormSectionHeader.test.tsx +74 -0
  157. package/src/components/Form/FormSectionHeader/FormSectionHeader.tsx +48 -0
  158. package/src/components/Form/FormSectionHeader/FormSectionHeader.types.ts +17 -0
  159. package/src/components/Form/FormSectionHeader/index.ts +2 -0
  160. package/src/components/Form/index.ts +17 -0
  161. package/src/index.ts +1 -0
@@ -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,2 @@
1
+ export { FieldLabel } from './FieldLabel';
2
+ export type { FieldLabelProps } from './FieldLabel.types';
@@ -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,2 @@
1
+ export { FieldRow } from './FieldRow';
2
+ export type { FieldRowProps } from './FieldRow.types';
@@ -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
+ });
@@ -0,0 +1,46 @@
1
+ import { Flex } from '@chakra-ui/react';
2
+
3
+ import { FormFooterProps } from './FormFooter.types';
4
+
5
+ /**
6
+ * The sticky footer shell shared by the form-in-sidebar surfaces — the studio
7
+ * settings footer and the image/video generation footers. It standardizes the
8
+ * chrome (sticky bottom, `bg.surface`, hairline top border) and lays out an
9
+ * optional left `start` slot against a right group of `meta` + `action`. The
10
+ * action's own loading/disabled state lives on the button passed in, so this is
11
+ * a pure layout shell.
12
+ */
13
+ export const FormFooter = ({
14
+ start,
15
+ meta,
16
+ action,
17
+ sticky = true,
18
+ ...rest
19
+ }: FormFooterProps) => {
20
+ return (
21
+ <Flex
22
+ {...(sticky ? { position: 'sticky', bottom: 0 } : { flexShrink: 0 })}
23
+ p={4}
24
+ borderTopWidth="1px"
25
+ borderColor="border.subtle"
26
+ w="100%"
27
+ bgColor="bg.surface"
28
+ justify={start ? 'space-between' : 'flex-end'}
29
+ align="center"
30
+ {...rest}
31
+ >
32
+ {start}
33
+ {(meta != null || action != null) && (
34
+ <Flex
35
+ align="center"
36
+ justify="flex-end"
37
+ gap={4}
38
+ flex={start ? undefined : 1}
39
+ >
40
+ {meta}
41
+ {action}
42
+ </Flex>
43
+ )}
44
+ </Flex>
45
+ );
46
+ };
@@ -0,0 +1,23 @@
1
+ import { ReactNode } from 'react';
2
+ import { FlexProps } from '@chakra-ui/react';
3
+
4
+ export interface FormFooterProps extends Omit<FlexProps, 'children'> {
5
+ /**
6
+ * Left-aligned slot — typically a destructive or back action (e.g. delete).
7
+ * When present the footer justifies its content `space-between`; otherwise the
8
+ * right group fills the width.
9
+ */
10
+ start?: ReactNode;
11
+ /**
12
+ * Right-aligned metadata rendered before the action — a saved-at timestamp or
13
+ * a credit cost.
14
+ */
15
+ meta?: ReactNode;
16
+ /** The primary action, right-aligned. */
17
+ action?: ReactNode;
18
+ /**
19
+ * Pin the footer to the bottom of its scroll container. Defaults to true; set
20
+ * false for a footer that flows at the end of non-scrolling content.
21
+ */
22
+ sticky?: boolean;
23
+ }
@@ -0,0 +1,2 @@
1
+ export { FormFooter } from './FormFooter';
2
+ export type { FormFooterProps } from './FormFooter.types';
@@ -0,0 +1,64 @@
1
+ import { Box } from '@chakra-ui/react';
2
+ import { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { FormModalFooter } from './FormModalFooter';
5
+
6
+ const meta: Meta<typeof FormModalFooter> = {
7
+ title: 'Components/Form/FormModalFooter',
8
+ component: FormModalFooter,
9
+ args: {
10
+ onCancel: () => {},
11
+ onSubmit: () => {},
12
+ cancelLabel: 'Cancel',
13
+ submitLabel: 'Save',
14
+ },
15
+ decorators: [
16
+ (Story) => (
17
+ <Box w="480px" bg="bg.canvas">
18
+ <Story />
19
+ </Box>
20
+ ),
21
+ ],
22
+ parameters: {
23
+ docs: {
24
+ description: {
25
+ component:
26
+ 'The action footer shared by form-in-a-modal surfaces — the form ' +
27
+ 'analog of a confirm-modal footer. Standardizes the optional delete ' +
28
+ '+ Cancel + primary-submit trio every create/edit modal was ' +
29
+ 'hand-rolling. Copy-agnostic: `cancelLabel` is required and labels ' +
30
+ 'are rendered verbatim.',
31
+ },
32
+ },
33
+ },
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof FormModalFooter>;
38
+
39
+ /** The common shape: Cancel + primary submit. */
40
+ export const Default: Story = {};
41
+
42
+ /** Edit modal shape: a far-left destructive Delete beside Cancel + submit. */
43
+ export const WithDelete: Story = {
44
+ args: {
45
+ onDelete: () => {},
46
+ deleteLabel: 'Delete',
47
+ submitLabel: 'Save changes',
48
+ },
49
+ };
50
+
51
+ /** Mid-request: spinner on submit, every button disabled. */
52
+ export const Submitting: Story = {
53
+ args: { isSubmitting: true },
54
+ };
55
+
56
+ /** Gated submit (e.g. a required field is empty). */
57
+ export const SubmitDisabled: Story = {
58
+ args: { submitDisabled: true },
59
+ };
60
+
61
+ /** Scroll-inside modal: the bordered separator against a pinned footer. */
62
+ export const Bordered: Story = {
63
+ args: { bordered: true, onDelete: () => {}, deleteLabel: 'Delete' },
64
+ };