@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.
Files changed (166) hide show
  1. package/dist/components/Button/Button.styles.d.ts.map +1 -1
  2. package/dist/components/Button/Button.styles.js +18 -12
  3. package/dist/components/Button/Button.styles.js.map +1 -1
  4. package/dist/components/Button/Button.styles.mjs +18 -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/components/Tag/Tag.styles.js +5 -5
  113. package/dist/components/Tag/Tag.styles.js.map +1 -1
  114. package/dist/components/Tag/Tag.styles.mjs +5 -5
  115. package/dist/components/Tag/Tag.styles.mjs.map +1 -1
  116. package/dist/index.d.ts +1 -0
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +18 -0
  119. package/dist/index.js.map +1 -1
  120. package/dist/index.mjs +9 -0
  121. package/dist/index.mjs.map +1 -1
  122. package/package.json +1 -1
  123. package/src/components/Button/Button.styles.ts +18 -12
  124. package/src/components/Card/Card.stories.tsx +7 -0
  125. package/src/components/Card/Card.styles.ts +8 -0
  126. package/src/components/Card/Card.types.ts +1 -1
  127. package/src/components/Form/FieldError/FieldError.test.tsx +33 -0
  128. package/src/components/Form/FieldError/FieldError.tsx +20 -0
  129. package/src/components/Form/FieldError/index.ts +2 -0
  130. package/src/components/Form/FieldHelp/FieldHelp.tsx +18 -0
  131. package/src/components/Form/FieldHelp/index.ts +2 -0
  132. package/src/components/Form/FieldLabel/FieldLabel.tsx +46 -0
  133. package/src/components/Form/FieldLabel/FieldLabel.types.ts +21 -0
  134. package/src/components/Form/FieldLabel/index.ts +2 -0
  135. package/src/components/Form/FieldRow/FieldRow.stories.tsx +85 -0
  136. package/src/components/Form/FieldRow/FieldRow.test.tsx +89 -0
  137. package/src/components/Form/FieldRow/FieldRow.tsx +46 -0
  138. package/src/components/Form/FieldRow/FieldRow.types.ts +32 -0
  139. package/src/components/Form/FieldRow/index.ts +2 -0
  140. package/src/components/Form/FormFooter/FormFooter.stories.tsx +70 -0
  141. package/src/components/Form/FormFooter/FormFooter.test.tsx +67 -0
  142. package/src/components/Form/FormFooter/FormFooter.tsx +46 -0
  143. package/src/components/Form/FormFooter/FormFooter.types.ts +23 -0
  144. package/src/components/Form/FormFooter/index.ts +2 -0
  145. package/src/components/Form/FormModalFooter/FormModalFooter.stories.tsx +64 -0
  146. package/src/components/Form/FormModalFooter/FormModalFooter.test.tsx +155 -0
  147. package/src/components/Form/FormModalFooter/FormModalFooter.tsx +83 -0
  148. package/src/components/Form/FormModalFooter/FormModalFooter.types.ts +53 -0
  149. package/src/components/Form/FormModalFooter/index.ts +2 -0
  150. package/src/components/Form/FormScrollArea/FormScrollArea.tsx +27 -0
  151. package/src/components/Form/FormScrollArea/index.ts +1 -0
  152. package/src/components/Form/FormSection/CollapsibleFormSection.tsx +35 -0
  153. package/src/components/Form/FormSection/FlatFormSection.tsx +41 -0
  154. package/src/components/Form/FormSection/FormSection.stories.tsx +115 -0
  155. package/src/components/Form/FormSection/FormSection.test.tsx +89 -0
  156. package/src/components/Form/FormSection/FormSection.tsx +33 -0
  157. package/src/components/Form/FormSection/FormSection.types.ts +34 -0
  158. package/src/components/Form/FormSection/index.ts +2 -0
  159. package/src/components/Form/FormSectionHeader/FormSectionHeader.stories.tsx +53 -0
  160. package/src/components/Form/FormSectionHeader/FormSectionHeader.test.tsx +74 -0
  161. package/src/components/Form/FormSectionHeader/FormSectionHeader.tsx +48 -0
  162. package/src/components/Form/FormSectionHeader/FormSectionHeader.types.ts +17 -0
  163. package/src/components/Form/FormSectionHeader/index.ts +2 -0
  164. package/src/components/Form/index.ts +17 -0
  165. package/src/components/Tag/Tag.styles.ts +5 -5
  166. package/src/index.ts +1 -0
@@ -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
+ };
@@ -0,0 +1,155 @@
1
+ import type { ReactNode } from 'react';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { FormModalFooter } from './FormModalFooter';
6
+
7
+ // Surface the Button's type/disabled/loading onto the DOM so behavior is
8
+ // testable without a Chakra provider. The copy-agnostic footer owns no
9
+ // translations, so labels are passed explicitly by the caller.
10
+ vi.mock('@/components/Modal', () => ({
11
+ ModalFooter: ({ children }: { children?: ReactNode }) => (
12
+ <div data-testid="footer">{children}</div>
13
+ ),
14
+ }));
15
+
16
+ vi.mock('@/components/Button', () => ({
17
+ Button: ({
18
+ children,
19
+ onClick,
20
+ type,
21
+ disabled,
22
+ loading,
23
+ }: {
24
+ children?: ReactNode;
25
+ onClick?: () => void;
26
+ type?: 'button' | 'submit';
27
+ disabled?: boolean;
28
+ loading?: boolean;
29
+ }) => (
30
+ <button
31
+ type={type}
32
+ onClick={onClick}
33
+ disabled={disabled}
34
+ data-loading={loading ? 'true' : 'false'}
35
+ >
36
+ {children}
37
+ </button>
38
+ ),
39
+ }));
40
+
41
+ vi.mock('@/components/Icon', () => ({
42
+ TrashIcon: () => <span data-testid="trash" />,
43
+ }));
44
+
45
+ const noop = () => {};
46
+
47
+ describe('FormModalFooter', () => {
48
+ it('renders a Cancel + submit pair with the given labels', () => {
49
+ render(
50
+ <FormModalFooter
51
+ onCancel={noop}
52
+ onSubmit={noop}
53
+ cancelLabel="Cancel"
54
+ submitLabel="Save"
55
+ />
56
+ );
57
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
58
+ expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
59
+ // No delete button unless onDelete is provided.
60
+ expect(screen.queryByTestId('trash')).not.toBeInTheDocument();
61
+ });
62
+
63
+ it('routes cancel and submit clicks to their handlers', () => {
64
+ const onCancel = vi.fn();
65
+ const onSubmit = vi.fn();
66
+ render(
67
+ <FormModalFooter
68
+ onCancel={onCancel}
69
+ onSubmit={onSubmit}
70
+ cancelLabel="Cancel"
71
+ submitLabel="Save"
72
+ />
73
+ );
74
+ fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
75
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }));
76
+ expect(onCancel).toHaveBeenCalledTimes(1);
77
+ expect(onSubmit).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it('is a type="submit" button inside a form (no onSubmit), type="button" with onSubmit', () => {
81
+ const { rerender } = render(
82
+ <FormModalFooter
83
+ onCancel={noop}
84
+ cancelLabel="Cancel"
85
+ submitLabel="Save"
86
+ />
87
+ );
88
+ expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute(
89
+ 'type',
90
+ 'submit'
91
+ );
92
+ rerender(
93
+ <FormModalFooter
94
+ onCancel={noop}
95
+ onSubmit={noop}
96
+ cancelLabel="Cancel"
97
+ submitLabel="Save"
98
+ />
99
+ );
100
+ expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute(
101
+ 'type',
102
+ 'button'
103
+ );
104
+ });
105
+
106
+ it('renders the standard delete button and routes its click', () => {
107
+ const onDelete = vi.fn();
108
+ render(
109
+ <FormModalFooter
110
+ onCancel={noop}
111
+ onSubmit={noop}
112
+ cancelLabel="Cancel"
113
+ submitLabel="Save"
114
+ onDelete={onDelete}
115
+ deleteLabel="Delete"
116
+ />
117
+ );
118
+ expect(screen.getByTestId('trash')).toBeInTheDocument();
119
+ fireEvent.click(screen.getByRole('button', { name: /Delete/ }));
120
+ expect(onDelete).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ it('disables every button and shows the submit spinner while submitting', () => {
124
+ render(
125
+ <FormModalFooter
126
+ onCancel={noop}
127
+ onSubmit={noop}
128
+ cancelLabel="Cancel"
129
+ submitLabel="Save"
130
+ onDelete={noop}
131
+ deleteLabel="Delete"
132
+ isSubmitting
133
+ />
134
+ );
135
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeDisabled();
136
+ expect(screen.getByRole('button', { name: /Delete/ })).toBeDisabled();
137
+ const submit = screen.getByRole('button', { name: 'Save' });
138
+ expect(submit).toBeDisabled();
139
+ expect(submit).toHaveAttribute('data-loading', 'true');
140
+ });
141
+
142
+ it('disables only the submit button when submitDisabled is set', () => {
143
+ render(
144
+ <FormModalFooter
145
+ onCancel={noop}
146
+ onSubmit={noop}
147
+ cancelLabel="Cancel"
148
+ submitLabel="Save"
149
+ submitDisabled
150
+ />
151
+ );
152
+ expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
153
+ expect(screen.getByRole('button', { name: 'Cancel' })).not.toBeDisabled();
154
+ });
155
+ });
@@ -0,0 +1,83 @@
1
+ import { Button } from '@/components/Button';
2
+ import { TrashIcon } from '@/components/Icon';
3
+ import { ModalFooter } from '@/components/Modal';
4
+
5
+ import { FormModalFooterProps } from './FormModalFooter.types';
6
+
7
+ /**
8
+ * The action footer shared by form-in-a-modal surfaces — the form analog of a
9
+ * confirm-modal footer. It standardizes the button trio every create/edit modal
10
+ * hand-rolls: an optional far-left destructive action, a neutral Cancel, and the
11
+ * primary submit that carries its own loading + disabled state.
12
+ *
13
+ * Copy-agnostic by design: as a design-system component it owns no strings, so
14
+ * `cancelLabel` is required and `deleteLabel` is rendered verbatim — the
15
+ * consuming app supplies its own (translated) labels.
16
+ *
17
+ * Submit path: inside a `<form onSubmit>` leave `onSubmit` unset so the button
18
+ * stays `type="submit"` (Enter submits); for a portaled or no-`<form>` modal
19
+ * pass `onSubmit` and it becomes a `type="button"` onClick. `...rest` forwards to
20
+ * the logician `ModalFooter`, so callers pin it (`position="sticky"` /
21
+ * `flexShrink={0}`) or zero its padding (`px={0}`) per modal.
22
+ */
23
+ export const FormModalFooter = ({
24
+ onDelete,
25
+ deleteLabel,
26
+ onCancel,
27
+ cancelLabel,
28
+ submitLabel,
29
+ submitColorPalette = 'primary',
30
+ isSubmitting = false,
31
+ submitDisabled = false,
32
+ onSubmit,
33
+ bordered = false,
34
+ ...rest
35
+ }: FormModalFooterProps) => {
36
+ return (
37
+ <ModalFooter
38
+ justifyContent="flex-end"
39
+ gap={2}
40
+ {...(bordered
41
+ ? {
42
+ bg: 'bg.surface',
43
+ borderTop: '1px solid',
44
+ borderColor: 'border.subtle',
45
+ }
46
+ : {})}
47
+ {...rest}
48
+ >
49
+ {onDelete && (
50
+ <Button
51
+ colorPalette="danger"
52
+ variant="solid"
53
+ type="button"
54
+ onClick={onDelete}
55
+ disabled={isSubmitting}
56
+ mr="auto"
57
+ >
58
+ <TrashIcon boxSize="sm" />
59
+ {deleteLabel}
60
+ </Button>
61
+ )}
62
+ <Button
63
+ colorPalette="neutral"
64
+ variant="outline"
65
+ type="button"
66
+ onClick={onCancel}
67
+ disabled={isSubmitting}
68
+ >
69
+ {cancelLabel}
70
+ </Button>
71
+ <Button
72
+ colorPalette={submitColorPalette}
73
+ variant="solid"
74
+ type={onSubmit ? 'button' : 'submit'}
75
+ onClick={onSubmit}
76
+ loading={isSubmitting}
77
+ disabled={isSubmitting || submitDisabled}
78
+ >
79
+ {submitLabel}
80
+ </Button>
81
+ </ModalFooter>
82
+ );
83
+ };
@@ -0,0 +1,53 @@
1
+ import { ComponentProps, ReactNode } from 'react';
2
+
3
+ import { ButtonColorPalette } from '@/components/Button';
4
+ import { ModalFooter } from '@/components/Modal';
5
+
6
+ type ModalFooterProps = ComponentProps<typeof ModalFooter>;
7
+
8
+ export interface FormModalFooterProps extends Omit<
9
+ ModalFooterProps,
10
+ 'children' | 'onSubmit' | 'title'
11
+ > {
12
+ /**
13
+ * Optional left-aligned destructive action. Renders the standard danger
14
+ * delete button (trash glyph + label), pushed to the far left of the footer.
15
+ */
16
+ onDelete?: () => void;
17
+ /** Delete button label. Rendered verbatim (no default — the app owns copy). */
18
+ deleteLabel?: ReactNode;
19
+ /** Cancel/dismiss handler. */
20
+ onCancel: () => void;
21
+ /**
22
+ * Cancel button label. Required — a design-system component owns no copy, so
23
+ * the consuming app must supply the (translated) label.
24
+ */
25
+ cancelLabel: ReactNode;
26
+ /** Primary submit button label. */
27
+ submitLabel: ReactNode;
28
+ /** Palette for the primary button. Defaults to `primary`. */
29
+ submitColorPalette?: ButtonColorPalette;
30
+ /**
31
+ * In-flight state — shows the submit spinner and disables every button so the
32
+ * modal can't be dismissed or re-submitted mid-request.
33
+ */
34
+ isSubmitting?: boolean;
35
+ /**
36
+ * Extra disable condition for the primary button beyond `isSubmitting` — a
37
+ * pristine/invalid gate (e.g. `!name.trim()`).
38
+ */
39
+ submitDisabled?: boolean;
40
+ /**
41
+ * Submit via `onClick` instead of native form submission. Pass this for a
42
+ * portaled or no-`<form>` modal, where a `type="submit"` button can't reach
43
+ * the enclosing `<form>`. Omit inside a `<form onSubmit>` so the button stays
44
+ * `type="submit"` and Enter still submits.
45
+ */
46
+ onSubmit?: () => void;
47
+ /**
48
+ * Draw the scroll-separator chrome (surface background + top hairline). Set on
49
+ * a tall scroll-inside modal so the pinned footer reads as separate from the
50
+ * scrolling body; omit on a short modal.
51
+ */
52
+ bordered?: boolean;
53
+ }
@@ -0,0 +1,2 @@
1
+ export { FormModalFooter } from './FormModalFooter';
2
+ export type { FormModalFooterProps } from './FormModalFooter.types';
@@ -0,0 +1,27 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { Box, BoxProps } from '@chakra-ui/react';
3
+
4
+ /**
5
+ * The scrolling body of a form panel: a full-height flex column that scrolls
6
+ * vertically, so a `position="sticky" bottom={0}` `FormFooter` pins to its
7
+ * bottom edge. Extracted from `FloatingPanelCard` so the same "scroll column +
8
+ * pinned footer" geometry can back any left-panel form without pulling in the
9
+ * card chrome.
10
+ */
11
+ export const FormScrollArea = ({
12
+ children,
13
+ ...rest
14
+ }: PropsWithChildren<BoxProps>) => (
15
+ <Box
16
+ h="100%"
17
+ overflowY="auto"
18
+ position="relative"
19
+ display="flex"
20
+ flexDirection="column"
21
+ {...rest}
22
+ >
23
+ {children}
24
+ </Box>
25
+ );
26
+
27
+ FormScrollArea.displayName = 'FormScrollArea';
@@ -0,0 +1 @@
1
+ export { FormScrollArea } from './FormScrollArea';
@@ -0,0 +1,35 @@
1
+ import { VStack } from '@chakra-ui/react';
2
+
3
+ import { CollapsibleSection } from '@/components/Workflow';
4
+
5
+ import { FormSectionProps } from './FormSection.types';
6
+
7
+ type CollapsibleFormSectionProps = Pick<
8
+ FormSectionProps,
9
+ 'title' | 'action' | 'defaultOpen' | 'hasError' | 'gap' | 'children'
10
+ >;
11
+
12
+ /**
13
+ * The collapsible `FormSection` variant — delegates to the logician
14
+ * `CollapsibleSection` accordion (which owns its own layout, so container style
15
+ * overrides apply to the flat variant only). Requires a title.
16
+ */
17
+ export const CollapsibleFormSection = ({
18
+ title,
19
+ action,
20
+ defaultOpen = true,
21
+ hasError = false,
22
+ gap = 4,
23
+ children,
24
+ }: CollapsibleFormSectionProps) => (
25
+ <CollapsibleSection
26
+ label={title ?? ''}
27
+ defaultExpanded={defaultOpen}
28
+ hasError={hasError}
29
+ headerAction={action}
30
+ >
31
+ <VStack align="stretch" gap={gap} w="100%">
32
+ {children}
33
+ </VStack>
34
+ </CollapsibleSection>
35
+ );
@@ -0,0 +1,41 @@
1
+ import { Box, VStack } from '@chakra-ui/react';
2
+
3
+ import { FormSectionHeader } from '../FormSectionHeader';
4
+ import { FormSectionProps } from './FormSection.types';
5
+
6
+ type FlatFormSectionProps = Omit<
7
+ FormSectionProps,
8
+ 'collapsible' | 'defaultOpen' | 'hasError'
9
+ >;
10
+
11
+ /** The flat (non-collapsible) `FormSection` variant. */
12
+ export const FlatFormSection = ({
13
+ title,
14
+ titleAdornment,
15
+ description,
16
+ action,
17
+ divider = false,
18
+ gap = 4,
19
+ children,
20
+ ...boxProps
21
+ }: FlatFormSectionProps) => (
22
+ <Box
23
+ w="100%"
24
+ pt={divider ? 4 : undefined}
25
+ borderTopWidth={divider ? '1px' : undefined}
26
+ borderColor={divider ? 'border.subtle' : undefined}
27
+ {...boxProps}
28
+ >
29
+ {(title || titleAdornment || description || action) && (
30
+ <FormSectionHeader
31
+ title={title}
32
+ titleAdornment={titleAdornment}
33
+ description={description}
34
+ action={action}
35
+ />
36
+ )}
37
+ <VStack align="stretch" gap={gap} w="100%">
38
+ {children}
39
+ </VStack>
40
+ </Box>
41
+ );
@@ -0,0 +1,115 @@
1
+ import { VStack } from '@chakra-ui/react';
2
+ import { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { Button } from '@/components/Button';
5
+ import { InfoSprinkle } from '@/components/InfoSprinkle';
6
+ import { Input } from '@/components/Input';
7
+
8
+ import { FieldRow } from '../FieldRow';
9
+ import { FormSection } from './FormSection';
10
+
11
+ const meta: Meta<typeof FormSection> = {
12
+ title: 'Components/Form/FormSection',
13
+ component: FormSection,
14
+ args: {
15
+ title: 'Profile',
16
+ children: (
17
+ <>
18
+ <FieldRow label="Display name">
19
+ <Input placeholder="Enter a name" />
20
+ </FieldRow>
21
+ <FieldRow label="Tagline" description="A short one-line summary.">
22
+ <Input placeholder="Enter a tagline" />
23
+ </FieldRow>
24
+ </>
25
+ ),
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof FormSection>;
31
+
32
+ export const Flat: Story = {};
33
+
34
+ export const WithDescription: Story = {
35
+ args: { description: 'How this assistant introduces itself.' },
36
+ };
37
+
38
+ export const WithAction: Story = {
39
+ args: {
40
+ action: (
41
+ <Button size="sm" variant="outline">
42
+ Add
43
+ </Button>
44
+ ),
45
+ },
46
+ };
47
+
48
+ export const WithDivider: Story = {
49
+ args: { divider: true },
50
+ };
51
+
52
+ export const Collapsible: Story = {
53
+ args: { collapsible: true },
54
+ };
55
+
56
+ export const CollapsibleWithError: Story = {
57
+ args: {
58
+ title: 'Behavior',
59
+ collapsible: true,
60
+ defaultOpen: false,
61
+ hasError: true,
62
+ },
63
+ };
64
+
65
+ export const WithTitleAdornment: Story = {
66
+ args: {
67
+ title: 'Attached files',
68
+ description: 'Files this assistant can reference when answering.',
69
+ titleAdornment: <InfoSprinkle>Up to 20 files, 100MB each.</InfoSprinkle>,
70
+ },
71
+ };
72
+
73
+ /**
74
+ * Several sections stacked the way a real sidebar form composes them — a lead
75
+ * section, a divided follow-on, and a collapsible group — to show the shared
76
+ * heading/description/field rhythm holds across the whole panel.
77
+ */
78
+ export const PanelComposition: Story = {
79
+ render: () => (
80
+ <VStack align="stretch" gap={8} maxW="480px">
81
+ <FormSection
82
+ title="Profile"
83
+ description="How this assistant introduces itself."
84
+ >
85
+ <FieldRow label="Display name">
86
+ <Input placeholder="Enter a name" />
87
+ </FieldRow>
88
+ <FieldRow label="Tagline" description="A short one-line summary.">
89
+ <Input placeholder="Enter a tagline" />
90
+ </FieldRow>
91
+ </FormSection>
92
+
93
+ <FormSection
94
+ title="Connectors"
95
+ description="Live data sources this assistant can query."
96
+ divider
97
+ action={
98
+ <Button size="sm" variant="ghost">
99
+ Add
100
+ </Button>
101
+ }
102
+ >
103
+ <FieldRow label="Default source">
104
+ <Input placeholder="Select a connector" />
105
+ </FieldRow>
106
+ </FormSection>
107
+
108
+ <FormSection title="Advanced" collapsible defaultOpen={false}>
109
+ <FieldRow label="Rate limit" description="Requests per minute.">
110
+ <Input placeholder="60" />
111
+ </FieldRow>
112
+ </FormSection>
113
+ </VStack>
114
+ ),
115
+ };