@ankhorage/surface 6.0.0 → 7.0.0

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 (148) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/{components/field → features/form/field/adapters/inbound}/Field.d.ts +2 -2
  4. package/dist/features/form/field/adapters/inbound/Field.d.ts.map +1 -0
  5. package/dist/features/form/field/adapters/inbound/Field.js +40 -0
  6. package/dist/features/form/field/adapters/inbound/Field.js.map +1 -0
  7. package/dist/features/form/field/public.d.ts +3 -0
  8. package/dist/features/form/field/public.d.ts.map +1 -0
  9. package/dist/features/form/field/public.js +2 -0
  10. package/dist/features/form/field/public.js.map +1 -0
  11. package/dist/{components/switch → features/form/switch/adapters/inbound}/Switch.d.ts +1 -1
  12. package/dist/features/form/switch/adapters/inbound/Switch.d.ts.map +1 -0
  13. package/dist/features/form/switch/adapters/inbound/Switch.js +52 -0
  14. package/dist/features/form/switch/adapters/inbound/Switch.js.map +1 -0
  15. package/dist/features/form/switch/public.d.ts +3 -0
  16. package/dist/features/form/switch/public.d.ts.map +1 -0
  17. package/dist/features/form/switch/public.js +2 -0
  18. package/dist/features/form/switch/public.js.map +1 -0
  19. package/dist/features/form/text-input/adapters/inbound/TextInput.d.ts.map +1 -1
  20. package/dist/features/form/text-input/adapters/inbound/TextInput.js +8 -4
  21. package/dist/features/form/text-input/adapters/inbound/TextInput.js.map +1 -1
  22. package/dist/features/list/adapters/inbound/List.d.ts +5 -0
  23. package/dist/features/list/adapters/inbound/List.d.ts.map +1 -0
  24. package/dist/features/list/adapters/inbound/List.js +7 -0
  25. package/dist/features/list/adapters/inbound/List.js.map +1 -0
  26. package/dist/features/list/adapters/inbound/ListItem.d.ts +5 -0
  27. package/dist/features/list/adapters/inbound/ListItem.d.ts.map +1 -0
  28. package/dist/features/list/adapters/inbound/ListItem.js +59 -0
  29. package/dist/features/list/adapters/inbound/ListItem.js.map +1 -0
  30. package/dist/features/list/public.d.ts +4 -0
  31. package/dist/features/list/public.d.ts.map +1 -0
  32. package/dist/features/list/public.js +3 -0
  33. package/dist/features/list/public.js.map +1 -0
  34. package/dist/index.d.ts +6 -12
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/{components/field/types.d.ts → types/field.d.ts} +1 -1
  39. package/dist/types/field.d.ts.map +1 -0
  40. package/dist/types/field.js +2 -0
  41. package/dist/types/field.js.map +1 -0
  42. package/dist/types/list.d.ts +20 -0
  43. package/dist/types/list.d.ts.map +1 -0
  44. package/dist/types/list.js +2 -0
  45. package/dist/types/list.js.map +1 -0
  46. package/dist/{components/switch/types.d.ts → types/switch.d.ts} +4 -4
  47. package/dist/types/switch.d.ts.map +1 -0
  48. package/dist/types/switch.js +2 -0
  49. package/dist/types/switch.js.map +1 -0
  50. package/package.json +1 -1
  51. package/src/features/form/field/adapters/inbound/Field.tsx +82 -0
  52. package/src/features/form/field/public.ts +2 -0
  53. package/src/features/form/switch/adapters/inbound/Switch.tsx +123 -0
  54. package/src/features/form/switch/public.ts +2 -0
  55. package/src/features/form/text-input/adapters/inbound/TextInput.tsx +21 -5
  56. package/src/features/list/adapters/inbound/List.tsx +9 -0
  57. package/src/features/list/adapters/inbound/ListItem.tsx +128 -0
  58. package/src/features/list/public.test.ts +13 -0
  59. package/src/features/list/public.ts +3 -0
  60. package/src/index.test.ts +15 -11
  61. package/src/index.ts +6 -12
  62. package/src/types/list.ts +22 -0
  63. package/src/{components/switch/types.ts → types/switch.ts} +3 -3
  64. package/dist/components/field/Field.d.ts.map +0 -1
  65. package/dist/components/field/Field.js +0 -27
  66. package/dist/components/field/Field.js.map +0 -1
  67. package/dist/components/field/index.d.ts +0 -3
  68. package/dist/components/field/index.d.ts.map +0 -1
  69. package/dist/components/field/index.js +0 -2
  70. package/dist/components/field/index.js.map +0 -1
  71. package/dist/components/field/types.d.ts.map +0 -1
  72. package/dist/components/field/types.js +0 -2
  73. package/dist/components/field/types.js.map +0 -1
  74. package/dist/components/helper-text/HelperText.d.ts +0 -4
  75. package/dist/components/helper-text/HelperText.d.ts.map +0 -1
  76. package/dist/components/helper-text/HelperText.js +0 -8
  77. package/dist/components/helper-text/HelperText.js.map +0 -1
  78. package/dist/components/helper-text/index.d.ts +0 -3
  79. package/dist/components/helper-text/index.d.ts.map +0 -1
  80. package/dist/components/helper-text/index.js +0 -2
  81. package/dist/components/helper-text/index.js.map +0 -1
  82. package/dist/components/helper-text/types.d.ts +0 -9
  83. package/dist/components/helper-text/types.d.ts.map +0 -1
  84. package/dist/components/helper-text/types.js +0 -2
  85. package/dist/components/helper-text/types.js.map +0 -1
  86. package/dist/components/label/Label.d.ts +0 -4
  87. package/dist/components/label/Label.d.ts.map +0 -1
  88. package/dist/components/label/Label.js +0 -11
  89. package/dist/components/label/Label.js.map +0 -1
  90. package/dist/components/label/index.d.ts +0 -3
  91. package/dist/components/label/index.d.ts.map +0 -1
  92. package/dist/components/label/index.js +0 -2
  93. package/dist/components/label/index.js.map +0 -1
  94. package/dist/components/label/types.d.ts +0 -10
  95. package/dist/components/label/types.d.ts.map +0 -1
  96. package/dist/components/label/types.js +0 -2
  97. package/dist/components/label/types.js.map +0 -1
  98. package/dist/components/list-item/ListItem.d.ts +0 -5
  99. package/dist/components/list-item/ListItem.d.ts.map +0 -1
  100. package/dist/components/list-item/ListItem.js +0 -44
  101. package/dist/components/list-item/ListItem.js.map +0 -1
  102. package/dist/components/list-item/index.d.ts +0 -3
  103. package/dist/components/list-item/index.d.ts.map +0 -1
  104. package/dist/components/list-item/index.js +0 -2
  105. package/dist/components/list-item/index.js.map +0 -1
  106. package/dist/components/list-item/types.d.ts +0 -11
  107. package/dist/components/list-item/types.d.ts.map +0 -1
  108. package/dist/components/list-item/types.js +0 -2
  109. package/dist/components/list-item/types.js.map +0 -1
  110. package/dist/components/switch/Switch.d.ts.map +0 -1
  111. package/dist/components/switch/Switch.js +0 -68
  112. package/dist/components/switch/Switch.js.map +0 -1
  113. package/dist/components/switch/index.d.ts +0 -3
  114. package/dist/components/switch/index.d.ts.map +0 -1
  115. package/dist/components/switch/index.js +0 -2
  116. package/dist/components/switch/index.js.map +0 -1
  117. package/dist/components/switch/types.d.ts.map +0 -1
  118. package/dist/components/switch/types.js +0 -2
  119. package/dist/components/switch/types.js.map +0 -1
  120. package/dist/components/textarea/Textarea.d.ts +0 -4
  121. package/dist/components/textarea/Textarea.d.ts.map +0 -1
  122. package/dist/components/textarea/Textarea.js +0 -6
  123. package/dist/components/textarea/Textarea.js.map +0 -1
  124. package/dist/components/textarea/index.d.ts +0 -3
  125. package/dist/components/textarea/index.d.ts.map +0 -1
  126. package/dist/components/textarea/index.js +0 -2
  127. package/dist/components/textarea/index.js.map +0 -1
  128. package/dist/components/textarea/types.d.ts +0 -5
  129. package/dist/components/textarea/types.d.ts.map +0 -1
  130. package/dist/components/textarea/types.js +0 -2
  131. package/dist/components/textarea/types.js.map +0 -1
  132. package/src/components/field/Field.tsx +0 -52
  133. package/src/components/field/index.ts +0 -2
  134. package/src/components/helper-text/HelperText.tsx +0 -12
  135. package/src/components/helper-text/index.ts +0 -2
  136. package/src/components/helper-text/types.ts +0 -10
  137. package/src/components/label/Label.tsx +0 -23
  138. package/src/components/label/index.ts +0 -2
  139. package/src/components/label/types.ts +0 -11
  140. package/src/components/list-item/ListItem.tsx +0 -76
  141. package/src/components/list-item/index.ts +0 -2
  142. package/src/components/list-item/types.ts +0 -11
  143. package/src/components/switch/Switch.tsx +0 -113
  144. package/src/components/switch/index.ts +0 -2
  145. package/src/components/textarea/Textarea.tsx +0 -15
  146. package/src/components/textarea/index.ts +0 -2
  147. package/src/components/textarea/types.ts +0 -5
  148. /package/src/{components/field/types.ts → types/field.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import type React from 'react';
2
- import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
3
- import type { ButtonBaseProps } from '../../primitives/button-base';
4
- import type { SurfaceColor } from '../../surfaceColor';
2
+ import type { ControlSize } from '../internal/resolvers/resolveControlSize';
3
+ import type { ButtonBaseProps } from '../primitives/button-base';
4
+ import type { SurfaceColor } from '../surfaceColor';
5
5
  export interface SwitchProps extends Omit<ButtonBaseProps, 'accessibilityRole' | 'accessibilityState' | 'children' | 'onPress' | 'style'> {
6
6
  children?: React.ReactNode;
7
7
  checked?: boolean;
@@ -12,4 +12,4 @@ export interface SwitchProps extends Omit<ButtonBaseProps, 'accessibilityRole' |
12
12
  invalid?: boolean;
13
13
  readOnly?: boolean;
14
14
  }
15
- //# sourceMappingURL=types.d.ts.map
15
+ //# sourceMappingURL=switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../src/types/switch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,WAAW,WAAY,SAAQ,IAAI,CACvC,eAAe,EACf,mBAAmB,GAAG,oBAAoB,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAC9E;IACC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAC3D,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=switch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.js","sourceRoot":"","sources":["../../src/types/switch.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\nimport type { ControlSize } from '../internal/resolvers/resolveControlSize';\nimport type { ButtonBaseProps } from '../primitives/button-base';\nimport type { SurfaceColor } from '../surfaceColor';\n\nexport interface SwitchProps extends Omit<\n ButtonBaseProps,\n 'accessibilityRole' | 'accessibilityState' | 'children' | 'onPress' | 'style'\n> {\n children?: React.ReactNode;\n checked?: boolean;\n defaultChecked?: boolean;\n onCheckedChange?: ((checked: boolean) => void) | undefined;\n color?: SurfaceColor;\n size?: ControlSize;\n invalid?: boolean;\n readOnly?: boolean;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/surface",
3
3
  "type": "module",
4
- "version": "6.0.0",
4
+ "version": "7.0.0",
5
5
  "description": "Stable UI foundation for React Native and React Native Web.",
6
6
  "homepage": "https://github.com/ankhorage/surface#readme",
7
7
  "bugs": {
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ type ResolvedFieldPresentation,
5
+ resolveFieldPresentation,
6
+ resolveFieldState,
7
+ } from '../../../../../internal/resolvers';
8
+ import type { FieldProps } from '../../../../../types/field';
9
+ import { View } from '../../../../layout/public';
10
+ import { Text } from '../../../../typography/public';
11
+
12
+ /*** Composes a control with its label and helper or error message. */
13
+ export function Field({
14
+ children,
15
+ label,
16
+ helperText,
17
+ errorText,
18
+ required = false,
19
+ disabled = false,
20
+ invalid = false,
21
+ readOnly = false,
22
+ testID,
23
+ }: FieldProps) {
24
+ const hasErrorText = Boolean(errorText);
25
+ const presentation = resolveFieldPresentation(
26
+ resolveFieldState({ disabled, invalid: invalid || hasErrorText, readOnly }),
27
+ );
28
+ const message = hasErrorText ? errorText : helperText;
29
+
30
+ return (
31
+ <View gap="xs" testID={testID}>
32
+ {renderFieldLabel(label, required, presentation)}
33
+ <View>{children}</View>
34
+ {renderFieldMessage(message, presentation)}
35
+ </View>
36
+ );
37
+ }
38
+
39
+ /*** Renders an optional field label and required indicator. */
40
+ function renderFieldLabel(
41
+ label: React.ReactNode,
42
+ required: boolean,
43
+ presentation: ResolvedFieldPresentation,
44
+ ) {
45
+ if (label === undefined || label === null) return null;
46
+ const labelContent =
47
+ typeof label === 'string' || typeof label === 'number' ? (
48
+ <Text
49
+ color={presentation.labelColor}
50
+ emphasis={presentation.labelEmphasis}
51
+ variant="label"
52
+ weight="medium"
53
+ >
54
+ {label}
55
+ </Text>
56
+ ) : (
57
+ label
58
+ );
59
+
60
+ return (
61
+ <View align="center" direction="row" gap="xs">
62
+ <View flex={1}>{labelContent}</View>
63
+ {required ? (
64
+ <Text color="error" variant="label" weight="medium">
65
+ *
66
+ </Text>
67
+ ) : null}
68
+ </View>
69
+ );
70
+ }
71
+
72
+ /*** Renders an optional helper or error message using field presentation semantics. */
73
+ function renderFieldMessage(message: React.ReactNode, presentation: ResolvedFieldPresentation) {
74
+ if (message === undefined || message === null || message === '') return null;
75
+ if (typeof message !== 'string' && typeof message !== 'number') return message;
76
+
77
+ return (
78
+ <Text color={presentation.helperColor} emphasis={presentation.helperEmphasis} variant="caption">
79
+ {message}
80
+ </Text>
81
+ );
82
+ }
@@ -0,0 +1,2 @@
1
+ export type { FieldProps } from '../../../types/field';
2
+ export { Field } from './adapters/inbound/Field';
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ type InteractionState,
5
+ resolveFieldState,
6
+ resolveIndicatorSize,
7
+ resolveSelectionControlColors,
8
+ resolveSelectionControlNextChecked,
9
+ } from '../../../../../internal/resolvers';
10
+ import { useControllableState } from '../../../../../internal/useControllableState';
11
+ import { ButtonBase } from '../../../../../primitives/button-base';
12
+ import { useTheme } from '../../../../../theme/ThemeContext';
13
+ import type { SwitchProps } from '../../../../../types/switch';
14
+ import { View } from '../../../../layout/public';
15
+ import { Text } from '../../../../typography/public';
16
+
17
+ /*** Renders a controlled or uncontrolled accessible switch. */
18
+ export function Switch({
19
+ children,
20
+ checked,
21
+ defaultChecked = false,
22
+ onCheckedChange,
23
+ color = 'primary',
24
+ size = 'm',
25
+ disabled = false,
26
+ invalid = false,
27
+ readOnly = false,
28
+ accessibilityLabel,
29
+ testID,
30
+ ...props
31
+ }: SwitchProps) {
32
+ const { theme } = useTheme();
33
+ const [isChecked, setChecked] = useControllableState<boolean>({
34
+ value: checked,
35
+ defaultValue: defaultChecked,
36
+ onChange: onCheckedChange,
37
+ });
38
+ const nextChecked = resolveSelectionControlNextChecked({
39
+ checked: isChecked,
40
+ disabled,
41
+ kind: 'switch',
42
+ readOnly,
43
+ });
44
+
45
+ return (
46
+ <ButtonBase
47
+ {...props}
48
+ accessibilityLabel={accessibilityLabel}
49
+ accessibilityRole="switch"
50
+ accessibilityState={{ checked: isChecked }}
51
+ disabled={disabled}
52
+ onPress={nextChecked === null ? undefined : () => setChecked(nextChecked)}
53
+ testID={testID}
54
+ >
55
+ {(interactionState) =>
56
+ renderSwitchContent(
57
+ { children, color, disabled, invalid, isChecked, readOnly, size, theme },
58
+ interactionState,
59
+ )
60
+ }
61
+ </ButtonBase>
62
+ );
63
+ }
64
+
65
+ interface SwitchContentInput {
66
+ children: React.ReactNode;
67
+ color: NonNullable<SwitchProps['color']>;
68
+ disabled: boolean;
69
+ invalid: boolean;
70
+ isChecked: boolean;
71
+ readOnly: boolean;
72
+ size: NonNullable<SwitchProps['size']>;
73
+ theme: ReturnType<typeof useTheme>['theme'];
74
+ }
75
+
76
+ /*** Resolves switch presentation for one interaction state and renders the visual control. */
77
+ function renderSwitchContent(input: SwitchContentInput, interactionState: InteractionState) {
78
+ const fieldState = resolveFieldState({
79
+ disabled: input.disabled,
80
+ focused: interactionState.focused,
81
+ invalid: input.invalid,
82
+ readOnly: input.readOnly,
83
+ });
84
+ const colors = resolveSelectionControlColors(input.theme, {
85
+ checked: input.isChecked,
86
+ fieldState,
87
+ hovered: interactionState.hovered,
88
+ pressed: interactionState.pressed,
89
+ color: input.color,
90
+ });
91
+ const indicatorSize = resolveIndicatorSize(input.size);
92
+ const labelEmphasis =
93
+ colors.labelColor === input.theme.semantics.content.muted ? 'muted' : 'default';
94
+
95
+ return (
96
+ <View align="center" direction="row" opacity={colors.opacity}>
97
+ <View
98
+ bg={colors.trackColor}
99
+ borderColor={colors.borderColor}
100
+ borderWidth={1}
101
+ height={indicatorSize.switchHeight}
102
+ justify="center"
103
+ minWidth={indicatorSize.switchWidth}
104
+ px={2}
105
+ radius="full"
106
+ width={indicatorSize.switchWidth}
107
+ >
108
+ <View
109
+ alignSelf={input.isChecked ? 'flex-end' : 'flex-start'}
110
+ bg={colors.thumbColor}
111
+ height={indicatorSize.switchThumb}
112
+ radius="full"
113
+ width={indicatorSize.switchThumb}
114
+ />
115
+ </View>
116
+ {input.children ? (
117
+ <View ml="s">
118
+ <Text emphasis={labelEmphasis}>{input.children}</Text>
119
+ </View>
120
+ ) : null}
121
+ </View>
122
+ );
123
+ }
@@ -0,0 +1,2 @@
1
+ export type { SwitchProps } from '../../../types/switch';
2
+ export { Switch } from './adapters/inbound/Switch';
@@ -1,5 +1,12 @@
1
1
  import React from 'react';
2
- import { Platform, TextInput as ReactNativeTextInput, View, type ViewStyle } from 'react-native';
2
+ import {
3
+ Platform,
4
+ type StyleProp,
5
+ TextInput as ReactNativeTextInput,
6
+ type TextStyle,
7
+ View,
8
+ type ViewStyle,
9
+ } from 'react-native';
3
10
 
4
11
  import { resolveFocusRingStyles } from '../../../../../internal/resolvers';
5
12
  import { useTheme } from '../../../../../theme/ThemeContext';
@@ -39,9 +46,7 @@ export function TextInput(props: TextInputProps) {
39
46
  nativeProps.onBlur?.(event);
40
47
  }}
41
48
  onChangeText={(nextValue) => {
42
- if (interactionPolicy !== 'passive') {
43
- nativeProps.onChangeText?.(nextValue);
44
- }
49
+ if (interactionPolicy !== 'passive') nativeProps.onChangeText?.(nextValue);
45
50
  }}
46
51
  onFocus={(event) => {
47
52
  setFocused(true);
@@ -49,13 +54,24 @@ export function TextInput(props: TextInputProps) {
49
54
  }}
50
55
  placeholderTextColor={presentation.placeholderColor}
51
56
  readOnly={readOnly}
52
- style={[presentation.inputStyle, nativeProps.style]}
57
+ style={resolveInputStyle(presentation.inputStyle, nativeProps.multiline, nativeProps.style)}
53
58
  />
54
59
  {renderAccessory(trailingAccessory, presentation.accessorySpacing, 'trailing')}
55
60
  </View>
56
61
  );
57
62
  }
58
63
 
64
+ const multilineInputStyle: TextStyle = { textAlignVertical: 'top' };
65
+
66
+ /*** Resolves TextInput style composition including canonical multiline alignment. */
67
+ function resolveInputStyle(
68
+ presentationStyle: StyleProp<TextStyle>,
69
+ multiline: boolean | undefined,
70
+ style: StyleProp<TextStyle>,
71
+ ): StyleProp<TextStyle> {
72
+ return [presentationStyle, multiline ? multilineInputStyle : undefined, style];
73
+ }
74
+
59
75
  /*** Renders one optional TextInput accessory with directional spacing. */
60
76
  function renderAccessory(
61
77
  accessory: React.ReactNode,
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+
3
+ import type { ListProps } from '../../../../types/list';
4
+ import { View } from '../../../layout/public';
5
+
6
+ /*** Groups list items under one neutral Surface list boundary. */
7
+ export function List({ children, testID }: ListProps) {
8
+ return <View testID={testID}>{children}</View>;
9
+ }
@@ -0,0 +1,128 @@
1
+ import React from 'react';
2
+
3
+ import { ButtonBase } from '../../../../primitives/button-base';
4
+ import { useTheme } from '../../../../theme/ThemeContext';
5
+ import type { SurfaceTheme } from '../../../../theme/types';
6
+ import type { ListItemProps } from '../../../../types/list';
7
+ import { View } from '../../../layout/public';
8
+ import { Text } from '../../../typography/public';
9
+
10
+ /*** Renders a static or interactive list item with shared row geometry and interaction states. */
11
+ export function ListItem(props: ListItemProps) {
12
+ const { theme } = useTheme();
13
+ if (!props.onPress) return renderStaticListItem(props, theme);
14
+ return renderInteractiveListItem({ ...props, onPress: props.onPress }, theme);
15
+ }
16
+
17
+ /*** Renders a non-pressable list item. */
18
+ function renderStaticListItem(props: ListItemProps, theme: SurfaceTheme) {
19
+ return (
20
+ <ListItemContent
21
+ {...props}
22
+ backgroundColor={props.selected ? theme.semantics.selection.background : 'transparent'}
23
+ />
24
+ );
25
+ }
26
+
27
+ /*** Renders a pressable list item with shared interaction-state presentation. */
28
+ function renderInteractiveListItem(
29
+ props: ListItemProps & { onPress: NonNullable<ListItemProps['onPress']> },
30
+ theme: SurfaceTheme,
31
+ ) {
32
+ return (
33
+ <ButtonBase
34
+ accessibilityLabel={props.accessibilityLabel}
35
+ accessibilityRole="button"
36
+ accessibilityState={{ disabled: props.disabled ?? false, selected: props.selected ?? false }}
37
+ disabled={props.disabled ?? false}
38
+ interactionPolicy={props.interactionPolicy}
39
+ onPress={props.onPress}
40
+ testID={props.testID}
41
+ >
42
+ {(state) => (
43
+ <ListItemContent
44
+ {...props}
45
+ backgroundColor={resolveListItemBackground(theme, {
46
+ hovered: state.hovered,
47
+ pressed: state.pressed,
48
+ selected: props.selected ?? false,
49
+ })}
50
+ disabled={state.disabled}
51
+ testID={undefined}
52
+ />
53
+ )}
54
+ </ButtonBase>
55
+ );
56
+ }
57
+
58
+ interface ListItemBackgroundState {
59
+ hovered: boolean;
60
+ pressed: boolean;
61
+ selected: boolean;
62
+ }
63
+
64
+ type ListItemContentProps = Omit<
65
+ ListItemProps,
66
+ 'accessibilityLabel' | 'interactionPolicy' | 'onPress' | 'selected'
67
+ > & {
68
+ backgroundColor: string;
69
+ };
70
+
71
+ /*** Resolves the neutral list-item background for one interaction state. */
72
+ function resolveListItemBackground(theme: SurfaceTheme, state: ListItemBackgroundState) {
73
+ if (state.pressed) return theme.semantics.neutral.surfaceActive;
74
+ if (state.hovered) return theme.semantics.neutral.surfaceHover;
75
+ if (state.selected) return theme.semantics.selection.background;
76
+ return 'transparent';
77
+ }
78
+
79
+ /*** Renders the shared visual row content for static and interactive list items. */
80
+ function ListItemContent({
81
+ backgroundColor,
82
+ children,
83
+ compact = false,
84
+ description,
85
+ disabled = false,
86
+ leading,
87
+ testID,
88
+ title,
89
+ trailing,
90
+ }: ListItemContentProps) {
91
+ const content = children ?? renderDefaultContent(title, description);
92
+
93
+ return (
94
+ <View
95
+ align="center"
96
+ bg={backgroundColor}
97
+ direction="row"
98
+ gap="m"
99
+ opacity={disabled ? 0.72 : 1}
100
+ px="m"
101
+ py={compact ? 's' : 'm'}
102
+ testID={testID}
103
+ >
104
+ {leading !== undefined ? <View>{leading}</View> : null}
105
+ <View flex={1}>{content}</View>
106
+ {trailing !== undefined ? <View>{trailing}</View> : null}
107
+ </View>
108
+ );
109
+ }
110
+
111
+ /*** Renders the default title and description content when custom children are absent. */
112
+ function renderDefaultContent(title: React.ReactNode, description: React.ReactNode) {
113
+ if (title === undefined && description === undefined) return null;
114
+ return (
115
+ <View flex={1} gap="xs">
116
+ {title !== undefined ? (
117
+ <Text numberOfLines={1} variant="body" weight="medium">
118
+ {title}
119
+ </Text>
120
+ ) : null}
121
+ {description !== undefined ? (
122
+ <Text emphasis="muted" numberOfLines={2} variant="bodySmall">
123
+ {description}
124
+ </Text>
125
+ ) : null}
126
+ </View>
127
+ );
128
+ }
@@ -0,0 +1,13 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ test('list capability owns List and ListItem under one feature boundary', async () => {
4
+ const listSource = await Bun.file('src/features/list/adapters/inbound/List.tsx').text();
5
+ const listItemSource = await Bun.file('src/features/list/adapters/inbound/ListItem.tsx').text();
6
+
7
+ expect(listSource).toContain('export function List');
8
+ expect(listItemSource).toContain('export function ListItem');
9
+ expect(listItemSource).toContain('interactionPolicy={props.interactionPolicy}');
10
+ expect(listItemSource).toContain('selected');
11
+ expect(listItemSource).toContain('compact');
12
+ expect(listItemSource).not.toContain('components/list-item');
13
+ });
@@ -0,0 +1,3 @@
1
+ export type { ListItemProps, ListProps } from '../../types/list';
2
+ export { List } from './adapters/inbound/List';
3
+ export { ListItem } from './adapters/inbound/ListItem';
package/src/index.test.ts CHANGED
@@ -22,12 +22,15 @@ const expectedFeatureRootExports = [
22
22
  "export { Button, IconButton } from './features/button/public';",
23
23
  "export { Card } from './features/card/public';",
24
24
  "export { Checkbox } from './features/form/checkbox/public';",
25
+ "export { Field } from './features/form/field/public';",
25
26
  "export { Radio } from './features/form/radio/public';",
27
+ "export { Switch } from './features/form/switch/public';",
26
28
  "export { TextInput } from './features/form/text-input/public';",
27
29
  "export { Icon, SUPPORTED_ICON_PROVIDERS } from './features/icon/public';",
28
30
  "export { Image } from './features/image/public';",
29
31
  "export { KeyboardAvoidingView } from './features/keyboard-avoiding-view/public';",
30
32
  "export { Divider, Grid, ScrollView, View } from './features/layout/public';",
33
+ "export { List, ListItem } from './features/list/public';",
31
34
  "export { Modal } from './features/modal/public';",
32
35
  "export { PopoverMenu } from './features/popover-menu/public';",
33
36
  "export { Popover } from './features/popover/public';",
@@ -38,38 +41,39 @@ const expectedFeatureRootExports = [
38
41
  "export { Heading, Text } from './features/typography/public';",
39
42
  ] as const;
40
43
 
41
- const expectedLegacyRootExports = [
42
- "export { Field } from './components/field';",
43
- "export { HelperText } from './components/helper-text';",
44
- "export { Label } from './components/label';",
45
- "export { ListItem } from './components/list-item';",
46
- "export { Switch } from './components/switch';",
47
- "export { Textarea } from './components/textarea';",
44
+ const expectedCrossCuttingRootExports = [
48
45
  "export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
49
46
  "export * from './core/responsive';",
50
47
  ] as const;
51
48
 
52
49
  describe('feature-owned root barrel contract', () => {
53
- it('routes migrated UI through feature-owned public facades', () => {
50
+ it('routes UI through feature-owned public facades', () => {
54
51
  expectedFeatureRootExports.forEach((line) => expect(indexSource).toContain(line));
55
52
  });
56
53
 
57
- it('does not retain removed layout aliases or a legacy layout facade', () => {
54
+ it('does not retain removed component or layout facades and aliases', () => {
55
+ expect(indexSource).not.toContain("'./components/");
58
56
  expect(indexSource).not.toContain("'./layout");
59
57
  expect(indexSource).not.toMatch(/\bBoxProps\b/u);
60
58
  expect(indexSource).not.toMatch(/\bContainerProps\b/u);
61
59
  expect(indexSource).not.toMatch(/\bStackProps\b/u);
62
60
  expect(indexSource).not.toMatch(/\bScrollAreaProps\b/u);
61
+ expect(indexSource).not.toMatch(/\bTextareaProps\b/u);
62
+ expect(indexSource).not.toMatch(/\bHelperTextProps\b/u);
63
+ expect(indexSource).not.toMatch(/\bLabelProps\b/u);
63
64
  expect(indexSource).not.toMatch(/export\s*\{[^}]*\bBox\b/u);
64
65
  expect(indexSource).not.toMatch(/export\s*\{[^}]*\bContainer\b/u);
65
66
  expect(indexSource).not.toMatch(/export\s*\{[^}]*\bStack\b/u);
66
67
  expect(indexSource).not.toMatch(/export\s*\{[^}]*\bScrollArea\b/u);
68
+ expect(indexSource).not.toMatch(/export\s*\{[^}]*\bTextarea\b/u);
69
+ expect(indexSource).not.toMatch(/export\s*\{[^}]*\bHelperText\b/u);
70
+ expect(indexSource).not.toMatch(/export\s*\{[^}]*\bLabel\b/u);
67
71
  });
68
72
  });
69
73
 
70
74
  describe('remaining root barrel contract', () => {
71
- it('keeps still-unmigrated UI on its current public API until its feature migration', () => {
72
- expectedLegacyRootExports.forEach((line) => expect(indexSource).toContain(line));
75
+ it('keeps deliberate cross-cutting public APIs', () => {
76
+ expectedCrossCuttingRootExports.forEach((line) => expect(indexSource).toContain(line));
73
77
  });
74
78
 
75
79
  it('does not retain obsolete action-sheet, drawer, menu aliases, or navigation chrome', () => {
package/src/index.ts CHANGED
@@ -1,15 +1,3 @@
1
- export type { FieldProps } from './components/field';
2
- export { Field } from './components/field';
3
- export type { HelperTextProps } from './components/helper-text';
4
- export { HelperText } from './components/helper-text';
5
- export type { LabelProps } from './components/label';
6
- export { Label } from './components/label';
7
- export type { ListItemProps } from './components/list-item';
8
- export { ListItem } from './components/list-item';
9
- export type { SwitchProps } from './components/switch';
10
- export { Switch } from './components/switch';
11
- export type { TextareaProps } from './components/textarea';
12
- export { Textarea } from './components/textarea';
13
1
  export type { FontRuntime } from './context/FontContext';
14
2
  export { FontProvider, useFontContext } from './context/FontContext';
15
3
  export type { I18nInstance, TranslationRuntime, Translator } from './context/TranslationContext';
@@ -40,8 +28,12 @@ export type { CardProps } from './features/card/public';
40
28
  export { Card } from './features/card/public';
41
29
  export type { CheckboxProps } from './features/form/checkbox/public';
42
30
  export { Checkbox } from './features/form/checkbox/public';
31
+ export type { FieldProps } from './features/form/field/public';
32
+ export { Field } from './features/form/field/public';
43
33
  export type { RadioProps } from './features/form/radio/public';
44
34
  export { Radio } from './features/form/radio/public';
35
+ export type { SwitchProps } from './features/form/switch/public';
36
+ export { Switch } from './features/form/switch/public';
45
37
  export type { TextInputProps } from './features/form/text-input/public';
46
38
  export { TextInput } from './features/form/text-input/public';
47
39
  export type {
@@ -62,6 +54,8 @@ export type {
62
54
  export { KeyboardAvoidingView } from './features/keyboard-avoiding-view/public';
63
55
  export type { DividerProps, GridProps, ScrollViewProps, ViewProps } from './features/layout/public';
64
56
  export { Divider, Grid, ScrollView, View } from './features/layout/public';
57
+ export type { ListItemProps, ListProps } from './features/list/public';
58
+ export { List, ListItem } from './features/list/public';
65
59
  export type { ModalProps } from './features/modal/public';
66
60
  export { Modal } from './features/modal/public';
67
61
  export type {
@@ -0,0 +1,22 @@
1
+ import type React from 'react';
2
+
3
+ import type { InteractionPolicyProps } from '../interactionPolicy';
4
+
5
+ export interface ListProps {
6
+ children?: React.ReactNode;
7
+ testID?: string;
8
+ }
9
+
10
+ export interface ListItemProps extends InteractionPolicyProps {
11
+ children?: React.ReactNode;
12
+ title?: React.ReactNode;
13
+ description?: React.ReactNode;
14
+ leading?: React.ReactNode;
15
+ trailing?: React.ReactNode;
16
+ onPress?: (() => void) | undefined;
17
+ disabled?: boolean;
18
+ selected?: boolean;
19
+ compact?: boolean;
20
+ accessibilityLabel?: string;
21
+ testID?: string;
22
+ }
@@ -1,8 +1,8 @@
1
1
  import type React from 'react';
2
2
 
3
- import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
4
- import type { ButtonBaseProps } from '../../primitives/button-base';
5
- import type { SurfaceColor } from '../../surfaceColor';
3
+ import type { ControlSize } from '../internal/resolvers/resolveControlSize';
4
+ import type { ButtonBaseProps } from '../primitives/button-base';
5
+ import type { SurfaceColor } from '../surfaceColor';
6
6
 
7
7
  export interface SwitchProps extends Omit<
8
8
  ButtonBaseProps,
@@ -1 +0,0 @@
1
- {"version":3,"file":"Field.d.ts","sourceRoot":"","sources":["../../../src/components/field/Field.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,8EAA8E;AAC9E,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,KAAK,EACL,UAAU,EACV,SAAS,EACT,QAAgB,EAChB,QAAgB,EAChB,OAAe,EACf,QAAgB,EAChB,MAAM,GACP,EAAE,UAAU,qBAgCZ"}
@@ -1,27 +0,0 @@
1
- import React from 'react';
2
- import { View } from '../../features/layout/public';
3
- import { resolveFieldPresentation, resolveFieldState } from '../../internal/resolvers';
4
- import { HelperText } from '../helper-text';
5
- import { Label } from '../label';
6
- /*** Composes a label, control, and helper or error message into one field. */
7
- export function Field({ children, label, helperText, errorText, required = false, disabled = false, invalid = false, readOnly = false, testID, }) {
8
- const hasErrorText = Boolean(errorText);
9
- const fieldState = resolveFieldState({
10
- disabled,
11
- invalid: invalid || hasErrorText,
12
- readOnly,
13
- });
14
- const presentation = resolveFieldPresentation(fieldState);
15
- return (<View gap="xs" testID={testID}>
16
- {label ? (<Label color={presentation.labelColor} emphasis={presentation.labelEmphasis} required={required}>
17
- {label}
18
- </Label>) : null}
19
- <View>{children}</View>
20
- {hasErrorText ? (<HelperText color={presentation.helperColor} emphasis={presentation.helperEmphasis}>
21
- {errorText}
22
- </HelperText>) : helperText ? (<HelperText color={presentation.helperColor} emphasis={presentation.helperEmphasis}>
23
- {helperText}
24
- </HelperText>) : null}
25
- </View>);
26
- }
27
- //# sourceMappingURL=Field.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Field.js","sourceRoot":"","sources":["../../../src/components/field/Field.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAGjC,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,EACpB,QAAQ,EACR,KAAK,EACL,UAAU,EACV,SAAS,EACT,QAAQ,GAAG,KAAK,EAChB,QAAQ,GAAG,KAAK,EAChB,OAAO,GAAG,KAAK,EACf,QAAQ,GAAG,KAAK,EAChB,MAAM,GACK;IACX,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,iBAAiB,CAAC;QACnC,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,YAAY;QAChC,QAAQ;KACT,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,wBAAwB,CAAC,UAAU,CAAC,CAAC;IAE1D,OAAO,CACL,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAC5B;MAAA,CAAC,KAAK,CAAC,CAAC,CAAC,CACP,CAAC,KAAK,CACJ,KAAK,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,CAC/B,QAAQ,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CACrC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAEnB;UAAA,CAAC,KAAK,CACR;QAAA,EAAE,KAAK,CAAC,CACT,CAAC,CAAC,CAAC,IAAI,CACR;MAAA,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAI,CACtB;MAAA,CAAC,YAAY,CAAC,CAAC,CAAC,CACd,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,cAAc,CAAC,CACjF;UAAA,CAAC,SAAS,CACZ;QAAA,EAAE,UAAU,CAAC,CACd,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CACf,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,cAAc,CAAC,CACjF;UAAA,CAAC,UAAU,CACb;QAAA,EAAE,UAAU,CAAC,CACd,CAAC,CAAC,CAAC,IAAI,CACV;IAAA,EAAE,IAAI,CAAC,CACR,CAAC;AACJ,CAAC","sourcesContent":["import React from 'react';\n\nimport { View } from '../../features/layout/public';\nimport { resolveFieldPresentation, resolveFieldState } from '../../internal/resolvers';\nimport { HelperText } from '../helper-text';\nimport { Label } from '../label';\nimport type { FieldProps } from './types';\n\n/*** Composes a label, control, and helper or error message into one field. */\nexport function Field({\n children,\n label,\n helperText,\n errorText,\n required = false,\n disabled = false,\n invalid = false,\n readOnly = false,\n testID,\n}: FieldProps) {\n const hasErrorText = Boolean(errorText);\n const fieldState = resolveFieldState({\n disabled,\n invalid: invalid || hasErrorText,\n readOnly,\n });\n const presentation = resolveFieldPresentation(fieldState);\n\n return (\n <View gap=\"xs\" testID={testID}>\n {label ? (\n <Label\n color={presentation.labelColor}\n emphasis={presentation.labelEmphasis}\n required={required}\n >\n {label}\n </Label>\n ) : null}\n <View>{children}</View>\n {hasErrorText ? (\n <HelperText color={presentation.helperColor} emphasis={presentation.helperEmphasis}>\n {errorText}\n </HelperText>\n ) : helperText ? (\n <HelperText color={presentation.helperColor} emphasis={presentation.helperEmphasis}>\n {helperText}\n </HelperText>\n ) : null}\n </View>\n );\n}\n"]}
@@ -1,3 +0,0 @@
1
- export { Field } from './Field';
2
- export type { FieldProps } from './types';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/field/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
@@ -1,2 +0,0 @@
1
- export { Field } from './Field';
2
- //# sourceMappingURL=index.js.map