@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
@@ -46,11 +46,15 @@ export const buttonColorPaletteStyles: Record<
46
46
  },
47
47
  },
48
48
  soft: {
49
- borderColor: 'transparent',
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: 'transparent',
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: 'transparent',
109
+ borderColor: 'secondary.lighter',
106
110
  bgColor: 'secondary.extralight', // #F4E8FB
107
111
  color: 'secondary.dark', // #570D7D
108
112
  _hover: {
109
- borderColor: 'transparent',
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: 'transparent',
165
+ borderColor: 'danger.lighter',
162
166
  bgColor: 'danger.extralight', // #FBE8E9
163
167
  color: 'danger.dark', // #7D0D14
164
168
  _hover: {
165
- borderColor: 'transparent',
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: 'transparent',
221
+ borderColor: 'success.lighter',
218
222
  bgColor: 'success.extralight', // #E9FBE8
219
223
  color: 'success.dark', // #147D0D
220
224
  _hover: {
221
- borderColor: 'transparent',
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: 'transparent',
280
+ borderColor: 'warning.lighter',
277
281
  bgColor: 'warning.extralight', // #FBF6E8
278
282
  color: 'warning.dark', // #7D610D
279
283
  _hover: {
280
- borderColor: 'transparent',
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
- borderColor: 'transparent',
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: 'transparent',
345
+ borderColor: 'border.default',
340
346
  bgColor: 'bg.muted', // gray.100 / gray.1200 (_dark)
341
347
  },
342
348
  _active: {
@@ -22,3 +22,10 @@ export const Gradient: Story = {
22
22
  children: 'Gradient Card',
23
23
  },
24
24
  };
25
+
26
+ export const Elevated: Story = {
27
+ args: {
28
+ variant: 'elevated',
29
+ children: 'Elevated Card',
30
+ },
31
+ };
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  import { CardRootProps as ChakraCardRootProps } from '@chakra-ui/react';
2
2
 
3
- export type CardVariant = 'default' | 'gradient';
3
+ export type CardVariant = 'default' | 'gradient' | 'elevated';
4
4
 
5
5
  export type CardProps = ChakraCardRootProps & {
6
6
  clickable?: boolean;
@@ -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,2 @@
1
+ export type { FieldErrorProps } from './FieldError';
2
+ export { FieldError } from './FieldError';
@@ -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,2 @@
1
+ export type { FieldHelpProps } from './FieldHelp';
2
+ export { FieldHelp } from './FieldHelp';
@@ -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
+ });