@krapwoo/ds-viewer 0.1.1 → 0.3.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 (173) hide show
  1. package/README.md +182 -78
  2. package/dist/cli/agentsFile.js +31 -0
  3. package/dist/cli/config.js +26 -1
  4. package/dist/cli/dev.js +50 -5
  5. package/dist/cli/doctor.js +492 -0
  6. package/dist/cli/explain.js +151 -0
  7. package/dist/cli/githubAction.js +68 -0
  8. package/dist/cli/glob.js +8 -0
  9. package/dist/cli/init.js +152 -18
  10. package/dist/cli/main.js +115 -4
  11. package/dist/cli/pageIndex.js +27 -1
  12. package/dist/cli/props.js +170 -21
  13. package/dist/cli/staticPage.js +356 -0
  14. package/dist/cli/sync.js +28 -5
  15. package/dist/cli/workspace.js +49 -21
  16. package/dist/native/catalog/comparison.js +283 -0
  17. package/dist/native/catalog/tokens.js +119 -0
  18. package/dist/native/catalog/types.js +1 -0
  19. package/native/catalog/CatalogShell.tsx +6 -1
  20. package/native/catalog/CatalogSidebar.tsx +98 -5
  21. package/native/catalog/ComparisonGrid.tsx +9 -7
  22. package/native/catalog/SpacingScaleGallery.tsx +1 -1
  23. package/native/catalog/TokenRow.tsx +3 -2
  24. package/native/catalog/catalogNavigation.ts +1 -1
  25. package/native/catalog/comparison.ts +63 -5
  26. package/native/catalog/index.ts +8 -6
  27. package/native/catalog/logoLayout.ts +12 -0
  28. package/native/catalog/pageApi.ts +21 -3
  29. package/native/catalog/tokens.ts +5 -0
  30. package/native/catalog/types.ts +9 -6
  31. package/package.json +12 -6
  32. package/starter-kit/WHEN_TO_USE.md +124 -0
  33. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
  34. package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
  35. package/starter-kit/components/AnimatedChevron/index.ts +2 -0
  36. package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
  37. package/starter-kit/components/Avatar/Avatar.tsx +74 -0
  38. package/starter-kit/components/Avatar/index.ts +2 -0
  39. package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
  40. package/starter-kit/components/Badge/Badge.tsx +56 -0
  41. package/starter-kit/components/Badge/Badge.types.ts +18 -0
  42. package/starter-kit/components/Badge/index.ts +2 -0
  43. package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
  44. package/starter-kit/components/Banner/Banner.tsx +257 -0
  45. package/starter-kit/components/Banner/Banner.types.ts +38 -0
  46. package/starter-kit/components/Banner/index.ts +2 -0
  47. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
  48. package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
  49. package/starter-kit/components/BottomSheet/index.ts +2 -0
  50. package/starter-kit/components/Button/Button.catalog.tsx +99 -0
  51. package/starter-kit/components/Button/Button.tsx +173 -0
  52. package/starter-kit/components/Button/Button.types.ts +52 -0
  53. package/starter-kit/components/Button/index.ts +2 -0
  54. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
  55. package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
  56. package/starter-kit/components/ButtonGroup/index.ts +2 -0
  57. package/starter-kit/components/Card/Card.catalog.tsx +68 -0
  58. package/starter-kit/components/Card/Card.tsx +133 -0
  59. package/starter-kit/components/Card/index.ts +2 -0
  60. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
  61. package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
  62. package/starter-kit/components/Checkbox/index.ts +2 -0
  63. package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
  64. package/starter-kit/components/Dialog/Dialog.tsx +82 -0
  65. package/starter-kit/components/Dialog/index.ts +2 -0
  66. package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
  67. package/starter-kit/components/Divider/Divider.tsx +20 -0
  68. package/starter-kit/components/Divider/index.ts +2 -0
  69. package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
  70. package/starter-kit/components/Dock/Dock.tsx +58 -0
  71. package/starter-kit/components/Dock/index.ts +2 -0
  72. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
  73. package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
  74. package/starter-kit/components/Dropdown/index.ts +2 -0
  75. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
  76. package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
  77. package/starter-kit/components/EmptyState/index.ts +2 -0
  78. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
  79. package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
  80. package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
  81. package/starter-kit/components/FieldContainer/index.ts +2 -0
  82. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
  83. package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
  84. package/starter-kit/components/InputClearButton/index.ts +2 -0
  85. package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
  86. package/starter-kit/components/InputField/InputField.tsx +418 -0
  87. package/starter-kit/components/InputField/index.ts +2 -0
  88. package/starter-kit/components/List/List.catalog.tsx +30 -0
  89. package/starter-kit/components/List/List.tsx +36 -0
  90. package/starter-kit/components/List/index.ts +2 -0
  91. package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
  92. package/starter-kit/components/ListItem/ListItem.tsx +165 -0
  93. package/starter-kit/components/ListItem/index.ts +2 -0
  94. package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
  95. package/starter-kit/components/Loading/Loading.tsx +121 -0
  96. package/starter-kit/components/Loading/index.ts +2 -0
  97. package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
  98. package/starter-kit/components/Pill/Pill.tsx +181 -0
  99. package/starter-kit/components/Pill/Pill.types.ts +30 -0
  100. package/starter-kit/components/Pill/index.ts +2 -0
  101. package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
  102. package/starter-kit/components/PillRow/PillRow.tsx +140 -0
  103. package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
  104. package/starter-kit/components/PillRow/index.ts +2 -0
  105. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
  106. package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
  107. package/starter-kit/components/ProgressDots/index.ts +2 -0
  108. package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
  109. package/starter-kit/components/Radio/Radio.tsx +114 -0
  110. package/starter-kit/components/Radio/index.ts +2 -0
  111. package/starter-kit/components/Radio/usePressScale.ts +33 -0
  112. package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
  113. package/starter-kit/components/SearchField/SearchField.tsx +98 -0
  114. package/starter-kit/components/SearchField/index.ts +2 -0
  115. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
  116. package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
  117. package/starter-kit/components/SectionHeader/index.ts +2 -0
  118. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
  119. package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
  120. package/starter-kit/components/SegmentedToggle/index.ts +2 -0
  121. package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
  122. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
  123. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
  124. package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
  125. package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
  126. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
  127. package/starter-kit/components/Shimmer/index.ts +2 -0
  128. package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
  129. package/starter-kit/components/Surface/Surface.tsx +45 -0
  130. package/starter-kit/components/Surface/index.ts +2 -0
  131. package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
  132. package/starter-kit/components/Switch/Switch.tsx +129 -0
  133. package/starter-kit/components/Switch/index.ts +2 -0
  134. package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
  135. package/starter-kit/components/TextArea/TextArea.tsx +99 -0
  136. package/starter-kit/components/TextArea/index.ts +2 -0
  137. package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
  138. package/starter-kit/components/Toast/Toast.tsx +144 -0
  139. package/starter-kit/components/Toast/index.ts +2 -0
  140. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
  141. package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
  142. package/starter-kit/components/Tooltip/index.ts +2 -0
  143. package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
  144. package/starter-kit/components/TopNav/TopNav.tsx +77 -0
  145. package/starter-kit/components/TopNav/index.ts +2 -0
  146. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
  147. package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
  148. package/starter-kit/components/UnderlineTabs/index.ts +2 -0
  149. package/starter-kit/components/index.ts +36 -0
  150. package/starter-kit/components/statusColors.ts +27 -0
  151. package/starter-kit/icons/Icon.native.tsx +90 -0
  152. package/starter-kit/icons/index.ts +10 -0
  153. package/starter-kit/icons/paths.ts +623 -0
  154. package/starter-kit/icons/types.ts +69 -0
  155. package/starter-kit/pages/Colors.catalog.tsx +55 -0
  156. package/starter-kit/pages/Font.catalog.tsx +45 -0
  157. package/starter-kit/pages/Icons.catalog.tsx +51 -0
  158. package/starter-kit/pages/Manifest.catalog.tsx +24 -0
  159. package/starter-kit/pages/Motion.catalog.tsx +101 -0
  160. package/starter-kit/pages/Radius.catalog.tsx +35 -0
  161. package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
  162. package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
  163. package/starter-kit/pages/Shadow.catalog.tsx +34 -0
  164. package/starter-kit/pages/Spacing.catalog.tsx +12 -0
  165. package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
  166. package/starter-kit/pages/Typography.catalog.tsx +17 -0
  167. package/starter-kit/tokens/index.ts +50 -0
  168. package/starter-kit/tokens/motion.ts +90 -0
  169. package/starter-kit/tokens/palette.ts +111 -0
  170. package/starter-kit/tokens/scales.ts +106 -0
  171. package/starter-kit/tokens/semantic.ts +98 -0
  172. package/starter-kit/tokens/shadow.ts +36 -0
  173. package/starter-kit/tokens/typography.ts +72 -0
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
3
+ import { EmptyState } from './EmptyState';
4
+ import { DS_SEMANTIC } from '../../tokens';
5
+
6
+ export default defineCatalogPage({
7
+ component: 'EmptyState',
8
+ group: 'Layout',
9
+ description: 'A centred placeholder for a screen or section with nothing to show yet — no results, no saved items, a first-run state.',
10
+ whenToUse: "Fills the entire content area because there's nothing else to show. For a note that sits alongside other real content, use Banner instead.",
11
+ a11y: 'A plain View with text; the action(s) render as real Buttons inside a ButtonGroup, which carry their own accessibility.',
12
+ // No `variant` prop exists on EmptyState — its "Variants" column shows the minimal default
13
+ // (iconName omitted, so it falls back to the real default) — the description/action
14
+ // configurations live under "States".
15
+ variants: {
16
+ itemsFill: true,
17
+ items: [{ key: 'default', name: 'Default', node: <EmptyState title="No saved trips yet" /> }],
18
+ },
19
+ states: {
20
+ itemsFill: true,
21
+ items: [
22
+ {
23
+ key: 'description',
24
+ name: 'With description',
25
+ node: <EmptyState iconName="search" title="No results found" description="Try a different station or address." />,
26
+ },
27
+ {
28
+ key: 'action',
29
+ name: 'With action',
30
+ node: (
31
+ <EmptyState
32
+ iconName="waypoints"
33
+ title="No saved trips yet"
34
+ description="Your saved trips will show up here."
35
+ action={{ label: 'Start a trip', onPress: () => {} }}
36
+ />
37
+ ),
38
+ },
39
+ {
40
+ key: 'secondary-action',
41
+ name: 'With two actions',
42
+ props: { secondaryAction: true },
43
+ node: (
44
+ <EmptyState
45
+ iconName="waypoints"
46
+ title="No saved trips yet"
47
+ description="Your saved trips will show up here."
48
+ action={{ label: 'Start a trip', onPress: () => {} }}
49
+ secondaryAction={{ label: 'Not now', onPress: () => {} }}
50
+ />
51
+ ),
52
+ },
53
+ {
54
+ key: 'custom-color',
55
+ name: 'Custom avatar colour',
56
+ node: (
57
+ <EmptyState
58
+ iconName="triangle-alert"
59
+ title="Something went wrong"
60
+ description="Check your connection and try again."
61
+ backgroundColor={DS_SEMANTIC.emphasis.negative}
62
+ />
63
+ ),
64
+ },
65
+ ],
66
+ },
67
+ });
@@ -0,0 +1,88 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { Avatar } from '../Avatar';
4
+ import { Button } from '../Button';
5
+ import { ButtonGroup } from '../ButtonGroup';
6
+ import type { IconName } from '../../icons';
7
+ import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
8
+
9
+ const AVATAR_SIZE = 64;
10
+
11
+ export interface EmptyStateAction {
12
+ label: string;
13
+ onPress: () => void;
14
+ }
15
+
16
+ export interface EmptyStateProps {
17
+ /** Icon shown above the title, on an Avatar circle. Defaults to `users` — the same generic-person
18
+ * glyph Avatar's own icon fallback uses, a reasonable default when nothing more specific fits
19
+ * ("no saved trips", "no results"). Pass a more specific icon when one obviously fits. */
20
+ iconName?: IconName;
21
+ /** Override the avatar circle's fill — same prop, same meaning as Avatar's own `backgroundColor`.
22
+ * Defaults to Avatar's own default (a neutral muted surface). */
23
+ backgroundColor?: string;
24
+ /** Primary message. */
25
+ title: string;
26
+ /** Supporting line below the title — what's empty, or what to do about it. */
27
+ description?: string;
28
+ /** Primary action rendered below the description (e.g. "Start a trip"). */
29
+ action?: EmptyStateAction;
30
+ /** Optional second action, stacked below `action` as a tertiary button (e.g. "Not now"). Only
31
+ * rendered when `action` is also set — a secondary action with no primary one doesn't make sense. */
32
+ secondaryAction?: EmptyStateAction;
33
+ style?: StyleProp<ViewStyle>;
34
+ }
35
+
36
+ /**
37
+ * A centred placeholder for a screen or section with nothing to show yet — no results, no saved
38
+ * items, a first-run state. An Avatar (icon on a circular fill, not a bare icon — reuses the same
39
+ * component and default styling a real profile picture would use here), title, optional
40
+ * description, optional action(s), always in that order and always centred; for anything more
41
+ * custom (illustrations, more than two actions), compose your own layout instead of extending this
42
+ * one. `action`/`secondaryAction` render through `ButtonGroup`'s `vertical` layout — always `small`
43
+ * size (EmptyState's own actions read as a lighter-weight, secondary moment, never the primary CTA
44
+ * of the screen they sit in) — so the primary/tertiary pair already follows ButtonGroup's own
45
+ * composition rules (matching size, matching label-only configuration) with nothing extra to wire up.
46
+ */
47
+ export function EmptyState({ iconName = 'users', backgroundColor, title, description, action, secondaryAction, style }: EmptyStateProps) {
48
+ return (
49
+ <View style={[styles.container, style]}>
50
+ <Avatar iconName={iconName} size={AVATAR_SIZE} backgroundColor={backgroundColor} />
51
+ <Text style={styles.title}>{title}</Text>
52
+ {!!description && <Text style={styles.description}>{description}</Text>}
53
+ {action && (
54
+ <ButtonGroup variant="vertical" style={styles.actionWrap}>
55
+ <Button label={action.label} onPress={action.onPress} size="small" />
56
+ {secondaryAction && <Button variant="tertiary" label={secondaryAction.label} onPress={secondaryAction.onPress} size="small" />}
57
+ </ButtonGroup>
58
+ )}
59
+ </View>
60
+ );
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ container: {
65
+ alignItems: 'center',
66
+ paddingHorizontal: DS_SPACING[1200],
67
+ paddingVertical: DS_SPACING[2000],
68
+ gap: DS_SPACING[400],
69
+ },
70
+ title: {
71
+ ...DS_TYPOGRAPHY.labelMd,
72
+ color: DS_SEMANTIC.text.regular,
73
+ textAlign: 'center',
74
+ marginTop: DS_SPACING[400],
75
+ },
76
+ description: {
77
+ ...DS_TYPOGRAPHY.bodySm,
78
+ color: DS_SEMANTIC.text.muted,
79
+ textAlign: 'center',
80
+ },
81
+ actionWrap: {
82
+ marginTop: DS_SPACING[400],
83
+ // Overrides ButtonGroup vertical's own default full-width stretch (right for Dock's button bar,
84
+ // wrong here) — EmptyState's actions read as a secondary, content-sized moment, not a bar
85
+ // spanning the whole width.
86
+ alignItems: 'center',
87
+ },
88
+ });
@@ -0,0 +1,2 @@
1
+ export { EmptyState } from './EmptyState';
2
+ export type { EmptyStateProps } from './EmptyState';
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { Text, StyleSheet } from 'react-native';
3
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
4
+ import { FieldContainer } from './FieldContainer';
5
+ import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
6
+
7
+ const styles = StyleSheet.create({
8
+ // FieldContainer — a stand-in "field" body, since the real component only supplies the chrome.
9
+ fieldContainerDemo: { height: 56, justifyContent: 'center', paddingHorizontal: DS_SPACING[800] },
10
+ fieldContainerText: { ...DS_TYPOGRAPHY.bodyMd, color: DS_SEMANTIC.text.muted },
11
+ });
12
+
13
+ export default defineCatalogPage({
14
+ component: 'FieldContainer',
15
+ group: 'Sub-Parts',
16
+ description: 'The shared field chrome — white surface, medium radius, a 1px border that darkens on focus and mutes when disabled — behind InputField, TextArea, Dropdown, and SearchField, so all four share one visually-consistent field look instead of each re-implementing it.',
17
+ a11y: 'Renders a plain View by default. Passing onPress (without disabled) makes it a Pressable with accessibilityRole="button" and the given accessibilityLabel — a consumer building a real editable field (InputField\'s editable mode) relies on its own inner TextInput for accessibility instead.',
18
+ variants: {
19
+ itemsFill: true,
20
+ items: [
21
+ {
22
+ key: 'resting',
23
+ name: 'Resting',
24
+ node: (
25
+ <FieldContainer style={styles.fieldContainerDemo}>
26
+ <Text style={styles.fieldContainerText}>Field content</Text>
27
+ </FieldContainer>
28
+ ),
29
+ },
30
+ {
31
+ key: 'focused',
32
+ name: 'Focused',
33
+ props: { focused: true },
34
+ node: (
35
+ <FieldContainer focused style={styles.fieldContainerDemo}>
36
+ <Text style={styles.fieldContainerText}>Field content</Text>
37
+ </FieldContainer>
38
+ ),
39
+ },
40
+ {
41
+ key: 'disabled',
42
+ name: 'Disabled',
43
+ props: { disabled: true },
44
+ node: (
45
+ <FieldContainer disabled style={styles.fieldContainerDemo}>
46
+ <Text style={styles.fieldContainerText}>Field content</Text>
47
+ </FieldContainer>
48
+ ),
49
+ },
50
+ ],
51
+ },
52
+ states: {
53
+ itemsFill: true,
54
+ items: [
55
+ {
56
+ key: 'pressed',
57
+ name: 'Pressed (caller-driven)',
58
+ props: { pressed: true },
59
+ node: (
60
+ <FieldContainer pressed style={styles.fieldContainerDemo}>
61
+ <Text style={styles.fieldContainerText}>Field content</Text>
62
+ </FieldContainer>
63
+ ),
64
+ },
65
+ {
66
+ key: 'pressable',
67
+ name: 'Pressable (with onPress)',
68
+ node: (
69
+ <FieldContainer onPress={() => {}} accessibilityLabel="Field content" style={styles.fieldContainerDemo}>
70
+ <Text style={styles.fieldContainerText}>Field content</Text>
71
+ </FieldContainer>
72
+ ),
73
+ },
74
+ ],
75
+ },
76
+ });
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import { View, Pressable, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { DS_SEMANTIC, DS_RADIUS } from '../../tokens';
4
+ import { useSurfaceTone } from '../Surface';
5
+ import type { FieldContainerProps } from './FieldContainer.types';
6
+
7
+ /**
8
+ * The shared "field" chrome: medium radius, a 1px subtle border that darkens to border.dark on
9
+ * focus, and a fill that contrasts with whatever it's actually placed on — reads the ambient
10
+ * `useSurfaceTone()` (see `Surface`) rather than assuming one fixed background. On the default
11
+ * white tone it's a translucent recessed fill (the same adaptive-overlay mechanism SegmentedToggle's
12
+ * track and Button's own `secondary` variant use); on a `muted` surface (wrapped in
13
+ * `<Surface tone="muted">`) it flips to opaque white instead, so it never goes invisible against
14
+ * whatever's behind it either way. It owns *only* the container look — consumers (InputField,
15
+ * TextArea, SearchField) supply their own layout (height, padding, content) via `style` and
16
+ * children, so a single-line row and a multiline area stay visually consistent.
17
+ */
18
+ export function FieldContainer({
19
+ focused = false,
20
+ disabled = false,
21
+ onPress,
22
+ pressed = false,
23
+ accessibilityLabel,
24
+ accessibilityState,
25
+ style,
26
+ children,
27
+ }: FieldContainerProps) {
28
+ const onWhite = useSurfaceTone() === 'white';
29
+ const toneStyle = onWhite ? stylesOnWhite : stylesOnMuted;
30
+
31
+ const stylesFor = (isPressed: boolean) => [
32
+ styles.base,
33
+ toneStyle.rest,
34
+ disabled && toneStyle.disabled,
35
+ !disabled && focused && toneStyle.focused,
36
+ !disabled && isPressed && toneStyle.pressed,
37
+ style,
38
+ ];
39
+
40
+ if (onPress && !disabled) {
41
+ // Pressable owns the pressed feedback; OR in the caller-driven `pressed` so both sources work.
42
+ return (
43
+ <Pressable
44
+ onPress={onPress}
45
+ accessibilityRole="button"
46
+ accessibilityLabel={accessibilityLabel}
47
+ accessibilityState={accessibilityState}
48
+ style={({ pressed: p }) => stylesFor(p || pressed)}
49
+ >
50
+ {children}
51
+ </Pressable>
52
+ );
53
+ }
54
+
55
+ // Non-Pressable (e.g. editable, or a disabled picker) fields still honour the caller-driven
56
+ // pressed treatment. When the caller gave this container a name/state, keep it an accessible
57
+ // button element — a disabled picker must announce as "…, button, disabled", not vanish from
58
+ // the accessibility tree just because it lost its Pressable.
59
+ const stillAccessible = accessibilityLabel != null || accessibilityState != null;
60
+ return (
61
+ <View
62
+ style={stylesFor(pressed)}
63
+ accessible={stillAccessible || undefined}
64
+ accessibilityRole={stillAccessible ? 'button' : undefined}
65
+ accessibilityLabel={accessibilityLabel}
66
+ accessibilityState={accessibilityState}
67
+ >
68
+ {children}
69
+ </View>
70
+ );
71
+ }
72
+
73
+ const styles = StyleSheet.create({
74
+ base: {
75
+ borderRadius: DS_RADIUS.medium,
76
+ borderWidth: 1,
77
+ borderColor: DS_SEMANTIC.border.subtle,
78
+ },
79
+ });
80
+
81
+ // On a white surface: translucent recessed fill, tuned pressed/disabled versions of that same
82
+ // overlay (recessedPressed/recessedDisabled — not a second overlay stacked on top, see Button's own
83
+ // `secondary` variant for why).
84
+ const stylesOnWhite = StyleSheet.create({
85
+ rest: { backgroundColor: DS_SEMANTIC.surface.recessed },
86
+ focused: { borderColor: DS_SEMANTIC.border.dark },
87
+ pressed: { backgroundColor: DS_SEMANTIC.surface.recessedPressed },
88
+ disabled: { backgroundColor: DS_SEMANTIC.surface.recessedDisabled, borderColor: DS_SEMANTIC.border.subtle },
89
+ });
90
+
91
+ // On a muted surface: opaque white fill instead (recessed would barely read against an
92
+ // already-grey background) — pressed/disabled are the plain surface tokens for "a white
93
+ // surface, tapped" / "a white surface, muted-out", the same ones other white-fill surfaces use.
94
+ const stylesOnMuted = StyleSheet.create({
95
+ rest: { backgroundColor: DS_SEMANTIC.surface.white },
96
+ focused: { borderColor: DS_SEMANTIC.border.dark },
97
+ pressed: { backgroundColor: DS_SEMANTIC.surface.onTap },
98
+ disabled: { backgroundColor: DS_SEMANTIC.surface.muted, borderColor: DS_SEMANTIC.border.subtle },
99
+ });
@@ -0,0 +1,27 @@
1
+ import type React from 'react';
2
+ import type { AccessibilityState, StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface FieldContainerProps {
5
+ /** Focused → border darkens to border.dark. */
6
+ focused?: boolean;
7
+ /** Disabled → washed-out fill, non-interactive. The exact fill adapts to the ambient surface tone
8
+ * (see FieldContainer.tsx's per-tone StyleSheets). */
9
+ disabled?: boolean;
10
+ /** When set (and not disabled), the container is a Pressable with tap feedback — the pressed fill
11
+ * adapts to the ambient surface tone (see FieldContainer.tsx). */
12
+ onPress?: () => void;
13
+ /** Caller-driven pressed state — applies the same pressed treatment without an `onPress` Pressable
14
+ * (e.g. an editable field that drives this from the TextInput's onPressIn/onPressOut). */
15
+ pressed?: boolean;
16
+ /** Only used when onPress is set. */
17
+ accessibilityLabel?: string;
18
+ /** Forwarded to the container (Pressable or plain View) — lets a picker-style consumer announce
19
+ * `{ disabled, expanded }`. When set (or when `accessibilityLabel` is), even the non-Pressable
20
+ * branch stays an accessible `button` element, so e.g. a disabled Dropdown still announces as
21
+ * "…, button, disabled" instead of disappearing from the accessibility tree. */
22
+ accessibilityState?: AccessibilityState;
23
+ /** Layout the consumer adds on top of the shared chrome (height, padding, flex direction…). */
24
+ style?: StyleProp<ViewStyle>;
25
+ /** The field's own layout, height, padding, and content. */
26
+ children: React.ReactNode;
27
+ }
@@ -0,0 +1,2 @@
1
+ export { FieldContainer } from './FieldContainer';
2
+ export type { FieldContainerProps } from './FieldContainer.types';
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
3
+ import { InputClearButton } from './InputClearButton';
4
+
5
+ export default defineCatalogPage({
6
+ component: 'InputClearButton',
7
+ group: 'Sub-Parts',
8
+ description: 'The clear (×) button InputField and SearchField both show once a field is active and holds a value — a filled circle-x icon sized to reach the 44pt touch target via hitSlop, not visual size.',
9
+ a11y: 'A Pressable with accessibilityRole="button" and the given (or default "Clear field") accessibilityLabel; hitSlop of 10 on every side pads its 24×24 visual size out to the 44pt minimum.',
10
+ variants: {
11
+ items: [{ key: 'default', name: 'Default', node: <InputClearButton onPress={() => {}} /> }],
12
+ },
13
+ states: {
14
+ items: [
15
+ { key: 'custom-label', name: 'Custom accessibility label', node: <InputClearButton onPress={() => {}} accessibilityLabel="Clear search" /> },
16
+ ],
17
+ },
18
+ });
@@ -0,0 +1,63 @@
1
+ import React, { useState } from 'react';
2
+ import { Pressable, StyleSheet, Platform } from 'react-native';
3
+ import { DS_SEMANTIC, DS_RADIUS } from '../../tokens';
4
+ import { Icon } from '../../icons/Icon.native';
5
+
6
+ /** Matches iOS search-field clear control (circle + ×). */
7
+ const CLEAR_ICON_SIZE = 20;
8
+
9
+ export interface InputClearButtonProps {
10
+ /** Tap handler — typically clears the paired field and refocuses it. */
11
+ onPress: () => void;
12
+ /** Accessible name — InputField/SearchField pass a field-specific label (e.g. "Clear To"). */
13
+ accessibilityLabel?: string;
14
+ }
15
+
16
+ // Web-only: this button only ever renders while its paired field is focused/active — but this
17
+ // Pressable is a plain (non-focusable) element, so the browser's default mousedown behaviour blurs
18
+ // the field the instant the press starts. That blur triggers InputField/SearchField's own
19
+ // `focused` state to flip synchronously, which unmounts THIS Pressable before its own press event
20
+ // ever gets to fire — the clear action is silently dropped (confirmed: neither onPress nor
21
+ // onPressIn ran). Blocking the browser's default mousedown behaviour keeps the field focused
22
+ // through the whole press, so this Pressable is still mounted by the time onPress fires. Native has
23
+ // no such default-focus-shift-on-press behaviour, so this is web-only.
24
+ const preventFocusSteal = Platform.OS === 'web' ? { onMouseDown: (e: { preventDefault: () => void }) => e.preventDefault() } : null;
25
+
26
+ export function InputClearButton({
27
+ onPress,
28
+ accessibilityLabel = 'Clear field',
29
+ }: InputClearButtonProps) {
30
+ const [focused, setFocused] = useState(false);
31
+ return (
32
+ <Pressable
33
+ onPress={onPress}
34
+ onFocus={() => setFocused(true)}
35
+ onBlur={() => setFocused(false)}
36
+ {...preventFocusSteal}
37
+ // 24x24 visual + 10 on every side reaches the 44pt touch-target minimum.
38
+ hitSlop={10}
39
+ // Keyboard focus shows the same highlight as a press (same policy as ListItem/SegmentedToggle).
40
+ style={({ pressed }) => [styles.button, (pressed || focused) && styles.buttonPressed]}
41
+ accessibilityRole="button"
42
+ accessibilityLabel={accessibilityLabel}
43
+ >
44
+ {/* A solid filled icon reads much heavier than the old stroke × — muted keeps it a quiet
45
+ utility action rather than competing with the field's own value text. */}
46
+ <Icon name="circle-x" size={CLEAR_ICON_SIZE} color={DS_SEMANTIC.text.muted} />
47
+ </Pressable>
48
+ );
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ button: {
53
+ width: 24,
54
+ height: 24,
55
+ borderRadius: DS_RADIUS.round,
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ flexShrink: 0,
59
+ },
60
+ buttonPressed: {
61
+ backgroundColor: DS_SEMANTIC.interaction.pressed,
62
+ },
63
+ });
@@ -0,0 +1,2 @@
1
+ export { InputClearButton } from './InputClearButton';
2
+ export type { InputClearButtonProps } from './InputClearButton';
@@ -0,0 +1,76 @@
1
+ import React, { useState } from 'react';
2
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
3
+ import { InputField } from './InputField';
4
+
5
+ function InputFieldDemo() {
6
+ const [value, setValue] = useState('');
7
+ return (
8
+ <InputField
9
+ label="From"
10
+ value={value}
11
+ onChangeText={setValue}
12
+ editable
13
+ placeholder="Search a station"
14
+ />
15
+ );
16
+ }
17
+
18
+ export default defineCatalogPage({
19
+ component: 'InputField',
20
+ group: 'Inputs',
21
+ description: 'A floating-label field. Resting: a centred, body-sized label with no border. Active (focused, or picker with active set) or filled: the label floats to a small top caption, row 2 shows the value/input, and a border fades in while active.',
22
+ whenToUse: 'A named field with a fixed identity ("To", "Arrive by") across the interaction. For free-text search with no floating label, use SearchField; for one of a small known set of choices, use Dropdown.',
23
+ a11y: 'The editable mode is a live TextInput; the picker mode is a Pressable row. Either mode shows a clear button while active (focused, or active for a picker) with a value set. Provide a meaningful label.',
24
+ variants: {
25
+ itemsFill: true,
26
+ items: [
27
+ {
28
+ key: 'default',
29
+ name: 'Default',
30
+ props: { label: 'To' },
31
+ node: <InputField label="To" placeholder="Search a station" onPress={() => {}} />,
32
+ },
33
+ {
34
+ key: 'icon-label',
35
+ name: 'Icon label',
36
+ props: { label: 'icon' },
37
+ node: <InputField label="icon" labelIcon="flag" value="Custom stop" onPress={() => {}} />,
38
+ },
39
+ ],
40
+ },
41
+ states: {
42
+ itemsFill: true,
43
+ items: [
44
+ { key: 'editable', name: 'Editable', props: { label: 'From' }, node: <InputFieldDemo /> },
45
+ {
46
+ key: 'picker',
47
+ name: 'Picker (filled)',
48
+ node: <InputField label="To" value="Grand Central" placeholder="Search a station" onPress={() => {}} />,
49
+ },
50
+ {
51
+ key: 'active',
52
+ name: 'Active (external picker open)',
53
+ node: <InputField label="To" value="Grand Central" placeholder="Search a station" active onPress={() => {}} />,
54
+ },
55
+ {
56
+ key: 'optional',
57
+ name: 'Optional label',
58
+ props: { label: 'Name' },
59
+ node: <InputField label="Name" placeholder="Nickname" optional onPress={() => {}} />,
60
+ },
61
+ {
62
+ key: 'value-icon',
63
+ name: 'Value with icon',
64
+ props: { label: 'Walk time' },
65
+ node: <InputField label="Walk time" value="8 min" valueIcon="footprints" />,
66
+ },
67
+ {
68
+ key: 'value-accent',
69
+ name: 'Accent value',
70
+ props: { label: 'Arrive by' },
71
+ node: <InputField label="Arrive by" value="9:12 AM" valueAccent />,
72
+ },
73
+ { key: 'disabled', name: 'Disabled', node: <InputField label="Arrive by" value="9:12 AM" disabled /> },
74
+ ],
75
+ },
76
+ });