@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.
- package/README.md +182 -78
- package/dist/cli/agentsFile.js +31 -0
- package/dist/cli/config.js +26 -1
- package/dist/cli/dev.js +50 -5
- package/dist/cli/doctor.js +492 -0
- package/dist/cli/explain.js +151 -0
- package/dist/cli/githubAction.js +68 -0
- package/dist/cli/glob.js +8 -0
- package/dist/cli/init.js +152 -18
- package/dist/cli/main.js +115 -4
- package/dist/cli/pageIndex.js +27 -1
- package/dist/cli/props.js +170 -21
- package/dist/cli/staticPage.js +356 -0
- package/dist/cli/sync.js +28 -5
- package/dist/cli/workspace.js +49 -21
- package/dist/native/catalog/comparison.js +283 -0
- package/dist/native/catalog/tokens.js +119 -0
- package/dist/native/catalog/types.js +1 -0
- package/native/catalog/CatalogShell.tsx +6 -1
- package/native/catalog/CatalogSidebar.tsx +98 -5
- package/native/catalog/ComparisonGrid.tsx +9 -7
- package/native/catalog/SpacingScaleGallery.tsx +1 -1
- package/native/catalog/TokenRow.tsx +3 -2
- package/native/catalog/catalogNavigation.ts +1 -1
- package/native/catalog/comparison.ts +63 -5
- package/native/catalog/index.ts +8 -6
- package/native/catalog/logoLayout.ts +12 -0
- package/native/catalog/pageApi.ts +21 -3
- package/native/catalog/tokens.ts +5 -0
- package/native/catalog/types.ts +9 -6
- package/package.json +12 -6
- package/starter-kit/WHEN_TO_USE.md +124 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
- package/starter-kit/components/AnimatedChevron/index.ts +2 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
- package/starter-kit/components/Avatar/Avatar.tsx +74 -0
- package/starter-kit/components/Avatar/index.ts +2 -0
- package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
- package/starter-kit/components/Badge/Badge.tsx +56 -0
- package/starter-kit/components/Badge/Badge.types.ts +18 -0
- package/starter-kit/components/Badge/index.ts +2 -0
- package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
- package/starter-kit/components/Banner/Banner.tsx +257 -0
- package/starter-kit/components/Banner/Banner.types.ts +38 -0
- package/starter-kit/components/Banner/index.ts +2 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
- package/starter-kit/components/BottomSheet/index.ts +2 -0
- package/starter-kit/components/Button/Button.catalog.tsx +99 -0
- package/starter-kit/components/Button/Button.tsx +173 -0
- package/starter-kit/components/Button/Button.types.ts +52 -0
- package/starter-kit/components/Button/index.ts +2 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
- package/starter-kit/components/ButtonGroup/index.ts +2 -0
- package/starter-kit/components/Card/Card.catalog.tsx +68 -0
- package/starter-kit/components/Card/Card.tsx +133 -0
- package/starter-kit/components/Card/index.ts +2 -0
- package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
- package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
- package/starter-kit/components/Checkbox/index.ts +2 -0
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
- package/starter-kit/components/Dialog/Dialog.tsx +82 -0
- package/starter-kit/components/Dialog/index.ts +2 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
- package/starter-kit/components/Divider/Divider.tsx +20 -0
- package/starter-kit/components/Divider/index.ts +2 -0
- package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
- package/starter-kit/components/Dock/Dock.tsx +58 -0
- package/starter-kit/components/Dock/index.ts +2 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
- package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
- package/starter-kit/components/Dropdown/index.ts +2 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
- package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
- package/starter-kit/components/EmptyState/index.ts +2 -0
- package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
- package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
- package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
- package/starter-kit/components/FieldContainer/index.ts +2 -0
- package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
- package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
- package/starter-kit/components/InputClearButton/index.ts +2 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
- package/starter-kit/components/InputField/InputField.tsx +418 -0
- package/starter-kit/components/InputField/index.ts +2 -0
- package/starter-kit/components/List/List.catalog.tsx +30 -0
- package/starter-kit/components/List/List.tsx +36 -0
- package/starter-kit/components/List/index.ts +2 -0
- package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
- package/starter-kit/components/ListItem/ListItem.tsx +165 -0
- package/starter-kit/components/ListItem/index.ts +2 -0
- package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
- package/starter-kit/components/Loading/Loading.tsx +121 -0
- package/starter-kit/components/Loading/index.ts +2 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
- package/starter-kit/components/Pill/Pill.tsx +181 -0
- package/starter-kit/components/Pill/Pill.types.ts +30 -0
- package/starter-kit/components/Pill/index.ts +2 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
- package/starter-kit/components/PillRow/PillRow.tsx +140 -0
- package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
- package/starter-kit/components/PillRow/index.ts +2 -0
- package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
- package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
- package/starter-kit/components/ProgressDots/index.ts +2 -0
- package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
- package/starter-kit/components/Radio/Radio.tsx +114 -0
- package/starter-kit/components/Radio/index.ts +2 -0
- package/starter-kit/components/Radio/usePressScale.ts +33 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
- package/starter-kit/components/SearchField/SearchField.tsx +98 -0
- package/starter-kit/components/SearchField/index.ts +2 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
- package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
- package/starter-kit/components/SectionHeader/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
- package/starter-kit/components/SegmentedToggle/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
- package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
- package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
- package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
- package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
- package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
- package/starter-kit/components/Shimmer/index.ts +2 -0
- package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
- package/starter-kit/components/Surface/Surface.tsx +45 -0
- package/starter-kit/components/Surface/index.ts +2 -0
- package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
- package/starter-kit/components/Switch/Switch.tsx +129 -0
- package/starter-kit/components/Switch/index.ts +2 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
- package/starter-kit/components/TextArea/TextArea.tsx +99 -0
- package/starter-kit/components/TextArea/index.ts +2 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
- package/starter-kit/components/Toast/Toast.tsx +144 -0
- package/starter-kit/components/Toast/index.ts +2 -0
- package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
- package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
- package/starter-kit/components/Tooltip/index.ts +2 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
- package/starter-kit/components/TopNav/TopNav.tsx +77 -0
- package/starter-kit/components/TopNav/index.ts +2 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
- package/starter-kit/components/UnderlineTabs/index.ts +2 -0
- package/starter-kit/components/index.ts +36 -0
- package/starter-kit/components/statusColors.ts +27 -0
- package/starter-kit/icons/Icon.native.tsx +90 -0
- package/starter-kit/icons/index.ts +10 -0
- package/starter-kit/icons/paths.ts +623 -0
- package/starter-kit/icons/types.ts +69 -0
- package/starter-kit/pages/Colors.catalog.tsx +55 -0
- package/starter-kit/pages/Font.catalog.tsx +45 -0
- package/starter-kit/pages/Icons.catalog.tsx +51 -0
- package/starter-kit/pages/Manifest.catalog.tsx +24 -0
- package/starter-kit/pages/Motion.catalog.tsx +101 -0
- package/starter-kit/pages/Radius.catalog.tsx +35 -0
- package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
- package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
- package/starter-kit/pages/Shadow.catalog.tsx +34 -0
- package/starter-kit/pages/Spacing.catalog.tsx +12 -0
- package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
- package/starter-kit/pages/Typography.catalog.tsx +17 -0
- package/starter-kit/tokens/index.ts +50 -0
- package/starter-kit/tokens/motion.ts +90 -0
- package/starter-kit/tokens/palette.ts +111 -0
- package/starter-kit/tokens/scales.ts +106 -0
- package/starter-kit/tokens/semantic.ts +98 -0
- package/starter-kit/tokens/shadow.ts +36 -0
- package/starter-kit/tokens/typography.ts +72 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { View, Animated, Easing, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_RADIUS, DS_SPACING, DS_MOTION_LOOP_DURATION } from '../../tokens';
|
|
4
|
+
import type { ShimmerProps, ShimmerVariant } from './Shimmer.types';
|
|
5
|
+
|
|
6
|
+
// True for any Shimmer rendered inside a <SkeletonGroup>. When set, individual blocks drop their own
|
|
7
|
+
// "Loading" announcement — the group announces once for the whole skeleton instead (see below).
|
|
8
|
+
const SkeletonGroupContext = createContext(false);
|
|
9
|
+
|
|
10
|
+
export const SHIMMER_TEXT_HEIGHT = DS_SPACING[800];
|
|
11
|
+
|
|
12
|
+
// Plain Shimmer blocks pulse between these two values. Translucent black (not an opaque grey) so the
|
|
13
|
+
// shimmer reads correctly as a wash over whatever surface it's placed on — light, dark, or coloured —
|
|
14
|
+
// the same convention DS uses for its other "works on any surface" overlays. Interpolating the colour
|
|
15
|
+
// itself (not a separate `opacity` style) keeps each independent block visually equivalent to a plain
|
|
16
|
+
// alpha wash.
|
|
17
|
+
const PULSE_LOW = 'rgba(0, 0, 0, 0.02)';
|
|
18
|
+
const PULSE_HIGH = 'rgba(0, 0, 0, 0.05)';
|
|
19
|
+
|
|
20
|
+
// One pulse cycle, shared by every Shimmer instance.
|
|
21
|
+
const PULSE_CYCLE_MS = DS_MOTION_LOOP_DURATION.pulse;
|
|
22
|
+
|
|
23
|
+
// A single module-level clock every Shimmer/Breathing instance reads from, instead of each starting
|
|
24
|
+
// its own timer at mount. Two independent per-instance timers would drift out of phase depending on
|
|
25
|
+
// the exact moment each mounted — sharing one clock means any shimmers visible together always pulse
|
|
26
|
+
// in lockstep. `useNativeDriver:false` is required because we interpolate `backgroundColor` (colour
|
|
27
|
+
// interpolation is JS-driven).
|
|
28
|
+
const sharedProgress = new Animated.Value(0);
|
|
29
|
+
let sharedProgressStarted = false;
|
|
30
|
+
function ensureSharedProgressStarted(): void {
|
|
31
|
+
if (sharedProgressStarted) return;
|
|
32
|
+
sharedProgressStarted = true;
|
|
33
|
+
Animated.loop(
|
|
34
|
+
Animated.timing(sharedProgress, {
|
|
35
|
+
toValue: 1,
|
|
36
|
+
duration: PULSE_CYCLE_MS,
|
|
37
|
+
easing: Easing.linear,
|
|
38
|
+
useNativeDriver: false,
|
|
39
|
+
}),
|
|
40
|
+
).start();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Shared animated primitive ────────────────────────────────────────────────
|
|
44
|
+
// A solid block that breathes between `low` and `high` on the shared progress value. Exported so any
|
|
45
|
+
// composite skeleton can reuse the same pulse (and stay in lockstep with plain Shimmers).
|
|
46
|
+
export function Breathing({
|
|
47
|
+
phase = 0,
|
|
48
|
+
low = PULSE_LOW,
|
|
49
|
+
high = PULSE_HIGH,
|
|
50
|
+
style,
|
|
51
|
+
accessibilityRole,
|
|
52
|
+
accessibilityLabel,
|
|
53
|
+
}: {
|
|
54
|
+
phase?: number;
|
|
55
|
+
/** Override the pulse's low/high colours. Defaults to the shared translucent pulse. */
|
|
56
|
+
low?: string;
|
|
57
|
+
high?: string;
|
|
58
|
+
style?: StyleProp<ViewStyle>;
|
|
59
|
+
accessibilityRole?: 'progressbar';
|
|
60
|
+
accessibilityLabel?: string;
|
|
61
|
+
}) {
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
ensureSharedProgressStarted();
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
// (progress + phase) % 1, then a triangle wave: high at 0, low at 0.5, high at 1 — equivalent to the
|
|
67
|
+
// cosine breathe, driven off the one shared linear clock so all instances stay phase-locked.
|
|
68
|
+
const backgroundColor = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
Animated.modulo(Animated.add(sharedProgress, phase), 1).interpolate({
|
|
71
|
+
inputRange: [0, 0.5, 1],
|
|
72
|
+
outputRange: [high, low, high],
|
|
73
|
+
}),
|
|
74
|
+
[phase, low, high],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Animated.View
|
|
79
|
+
style={[style, { backgroundColor }]}
|
|
80
|
+
accessibilityRole={accessibilityRole}
|
|
81
|
+
accessibilityLabel={accessibilityLabel}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function dimensionsForVariant(
|
|
87
|
+
variant: ShimmerVariant,
|
|
88
|
+
width: ShimmerProps['width'],
|
|
89
|
+
height: ShimmerProps['height'],
|
|
90
|
+
size: number,
|
|
91
|
+
): ViewStyle {
|
|
92
|
+
switch (variant) {
|
|
93
|
+
case 'circle':
|
|
94
|
+
return { width: size, height: size, borderRadius: DS_RADIUS.round };
|
|
95
|
+
case 'container':
|
|
96
|
+
return { width: width ?? '100%', height: height ?? 80, borderRadius: DS_RADIUS.medium };
|
|
97
|
+
case 'text':
|
|
98
|
+
default:
|
|
99
|
+
return { width: width ?? '100%', height: height ?? SHIMMER_TEXT_HEIGHT, borderRadius: DS_RADIUS.small };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Shimmer ──────────────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* A single loading placeholder — see `ShimmerProps.variant` for what each shape stands in for.
|
|
106
|
+
* A real composite skeleton (e.g. a list row: avatar + a two-line label) is built by stacking
|
|
107
|
+
* multiple Shimmers of the right variant/count/dimensions, not by inventing one shape to cover
|
|
108
|
+
* everything — see the "Circle + text" example in the catalog for exactly this composition.
|
|
109
|
+
*/
|
|
110
|
+
export function Shimmer({ variant = 'text', width, height, size = 40, style }: ShimmerProps) {
|
|
111
|
+
// Inside a <SkeletonGroup>, the group owns the single "Loading" announcement — each block stays
|
|
112
|
+
// silent so a composite skeleton (avatar + two text lines = 3 blocks) doesn't make a screen
|
|
113
|
+
// reader read "Loading, Loading, Loading". A standalone Shimmer still announces on its own.
|
|
114
|
+
const insideGroup = useContext(SkeletonGroupContext);
|
|
115
|
+
return (
|
|
116
|
+
<Breathing
|
|
117
|
+
style={[dimensionsForVariant(variant, width, height, size), style]}
|
|
118
|
+
accessibilityRole={insideGroup ? undefined : 'progressbar'}
|
|
119
|
+
accessibilityLabel={insideGroup ? undefined : 'Loading'}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── SkeletonGroup ──────────────────────────────────────────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* Wraps a composite skeleton (several stacked Shimmers standing in for one real element — e.g. a
|
|
127
|
+
* list row's avatar + two text lines) so assistive tech announces it as ONE "Loading" region
|
|
128
|
+
* instead of once per block. Provides {@link SkeletonGroupContext} so every descendant Shimmer drops
|
|
129
|
+
* its own announcement; the group itself carries the single `progressbar`/busy role. Pass your own
|
|
130
|
+
* layout via `style` (the group is a plain View, so `flexDirection`/`gap` land where you'd expect) —
|
|
131
|
+
* it adds no layout of its own.
|
|
132
|
+
*/
|
|
133
|
+
export function SkeletonGroup({
|
|
134
|
+
label = 'Loading',
|
|
135
|
+
style,
|
|
136
|
+
children,
|
|
137
|
+
}: {
|
|
138
|
+
/** Accessible name for the whole loading region. @default 'Loading' */
|
|
139
|
+
label?: string;
|
|
140
|
+
/** Layout for the group (flexDirection, gap, …) — it renders a plain View, so styles land where
|
|
141
|
+
* you'd expect. */
|
|
142
|
+
style?: StyleProp<ViewStyle>;
|
|
143
|
+
/** The Shimmers making up the skeleton. */
|
|
144
|
+
children: ReactNode;
|
|
145
|
+
}) {
|
|
146
|
+
return (
|
|
147
|
+
<SkeletonGroupContext.Provider value>
|
|
148
|
+
<View
|
|
149
|
+
style={style}
|
|
150
|
+
accessibilityRole="progressbar"
|
|
151
|
+
accessibilityLabel={label}
|
|
152
|
+
accessibilityState={{ busy: true }}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</View>
|
|
156
|
+
</SkeletonGroupContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type ShimmerVariant = 'text' | 'container' | 'circle';
|
|
4
|
+
|
|
5
|
+
export interface ShimmerProps {
|
|
6
|
+
/** Which shape this placeholder stands in for:
|
|
7
|
+
* - `'text'` (default) — **one line of text**. A real element with 3 lines of text gets 3
|
|
8
|
+
* stacked `text` Shimmers, not one tall one — a skeleton should mirror the real line count.
|
|
9
|
+
* When stacking 2+ lines, give the last one ~70% of the others' width — real text rarely fills
|
|
10
|
+
* every line edge-to-edge, and a shorter final line reads as "this is where the paragraph
|
|
11
|
+
* naturally ends," not a loading glitch.
|
|
12
|
+
* - `'circle'` — always a circular element (an Avatar, an icon-only Pill/Button, a status dot) —
|
|
13
|
+
* never used for anything that isn't actually round in its loaded state.
|
|
14
|
+
* - `'container'` — everything else: a Card, a Banner, an image, any non-text/non-circular UI
|
|
15
|
+
* element. Its `width`/`height` should match the real element it's replacing (a Card skeleton
|
|
16
|
+
* should be Card-shaped, not an arbitrary block), so the skeleton reserves the same layout
|
|
17
|
+
* space the loaded content will actually occupy — no jump when it swaps in. */
|
|
18
|
+
variant?: ShimmerVariant;
|
|
19
|
+
/** Width (text and container). Defaults to 100%. */
|
|
20
|
+
width?: number | `${number}%`;
|
|
21
|
+
/** Height (text and container). Defaults: text 16, container 80 — override to match the real
|
|
22
|
+
* element this stands in for. */
|
|
23
|
+
height?: number;
|
|
24
|
+
/** Diameter for `circle` variant. Default 40. */
|
|
25
|
+
size?: number;
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Shimmer, SkeletonGroup } from './Shimmer';
|
|
5
|
+
import { DS_SPACING } from '../../tokens';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
shimmerLines: { flex: 1, gap: DS_SPACING[400] },
|
|
9
|
+
previewRow: { flexDirection: 'row', alignItems: 'center', gap: DS_SPACING[600] },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default defineCatalogPage({
|
|
13
|
+
component: 'SkeletonGroup',
|
|
14
|
+
group: 'Sub-Parts',
|
|
15
|
+
description: 'An accessibility wrapper for a composite skeleton — several Shimmers standing in for one real element (e.g. a list row: an avatar + a two-line label). It announces the whole thing as one "Loading" region and silences the individual blocks, so a screen reader says "Loading" once instead of once per Shimmer. Adds no layout of its own — pass your own flexDirection/gap via style.',
|
|
16
|
+
whenToUse: 'Wrap 2+ Shimmers that together stand in for one element. A single lone Shimmer already announces on its own and needs no wrapper.',
|
|
17
|
+
a11y: 'Carries accessibilityRole="progressbar" + accessibilityLabel (default "Loading") + accessibilityState={{ busy: true }} for the whole region; provides a context that makes every descendant Shimmer drop its own announcement. Net effect: one "Loading" announcement per skeleton, not one per block.',
|
|
18
|
+
// No visual variants/states — it's a transparent a11y wrapper. The one example shows the
|
|
19
|
+
// composite it wraps; Props + Accessibility document its contract.
|
|
20
|
+
hide: { states: true },
|
|
21
|
+
variants: {
|
|
22
|
+
itemsFill: true,
|
|
23
|
+
items: [
|
|
24
|
+
{
|
|
25
|
+
key: 'composite',
|
|
26
|
+
name: 'Wrapping a composite skeleton',
|
|
27
|
+
node: (
|
|
28
|
+
<SkeletonGroup style={styles.previewRow}>
|
|
29
|
+
<Shimmer variant="circle" size={40} />
|
|
30
|
+
<View style={styles.shimmerLines}>
|
|
31
|
+
<Shimmer variant="text" width={160} />
|
|
32
|
+
<Shimmer variant="text" width={112} />
|
|
33
|
+
</View>
|
|
34
|
+
</SkeletonGroup>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Surface } from './Surface';
|
|
5
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
surfaceDemo: { height: 96, justifyContent: 'center', alignItems: 'center', padding: DS_SPACING[800] },
|
|
9
|
+
surfaceText: { ...DS_TYPOGRAPHY.bodyMd, color: DS_SEMANTIC.text.regular },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default defineCatalogPage({
|
|
13
|
+
component: 'Surface',
|
|
14
|
+
group: 'Surfaces',
|
|
15
|
+
description:
|
|
16
|
+
"Declares \"this subtree's background is tone\" — renders a plain View filled with the matching token (surface.white or surface.main) and provides that tone to descendants via context, so a tone-aware component inside (e.g. FieldContainer/InputField) can automatically pick a fill that contrasts with it instead of assuming white.",
|
|
17
|
+
whenToUse:
|
|
18
|
+
"Wrap a screen (or any region) wherever its background isn't the default white — a page with a muted/grey background, for instance — so anything inside that needs to contrast with its background picks the right fill automatically.",
|
|
19
|
+
variants: {
|
|
20
|
+
itemsFill: true,
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
key: 'white',
|
|
24
|
+
name: 'White',
|
|
25
|
+
props: { tone: 'white' },
|
|
26
|
+
node: (
|
|
27
|
+
<Surface tone="white" style={styles.surfaceDemo}>
|
|
28
|
+
<Text style={styles.surfaceText}>surface.white</Text>
|
|
29
|
+
</Surface>
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: 'muted',
|
|
34
|
+
name: 'Muted',
|
|
35
|
+
props: { tone: 'muted' },
|
|
36
|
+
node: (
|
|
37
|
+
<Surface tone="muted" style={styles.surfaceDemo}>
|
|
38
|
+
<Text style={styles.surfaceText}>surface.main</Text>
|
|
39
|
+
</Surface>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC } from '../../tokens';
|
|
4
|
+
|
|
5
|
+
export type SurfaceTone = 'white' | 'muted';
|
|
6
|
+
|
|
7
|
+
// Defaults to 'white' — a screen with no explicit <Surface> ancestor is assumed to sit on a white
|
|
8
|
+
// background, the common case (matches e.g. Card's own default fill).
|
|
9
|
+
const SurfaceToneContext = createContext<SurfaceTone>('white');
|
|
10
|
+
|
|
11
|
+
/** What background tone the nearest ancestor `<Surface>` declared (or 'white' if there isn't one).
|
|
12
|
+
* Components that need to contrast with whatever's actually behind them — FieldContainer/
|
|
13
|
+
* InputField today — read this instead of assuming one fixed background. */
|
|
14
|
+
export function useSurfaceTone(): SurfaceTone {
|
|
15
|
+
return useContext(SurfaceToneContext);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SurfaceProps {
|
|
19
|
+
/** Which background this surface renders. Descendants read this via `useSurfaceTone()` to
|
|
20
|
+
* automatically pick a fill that contrasts with it (e.g. a field goes white on a muted page,
|
|
21
|
+
* recessed on a white one) instead of needing to be told individually. */
|
|
22
|
+
tone: SurfaceTone;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Declares "this subtree's background is `tone`" — renders a plain View filled with the matching
|
|
29
|
+
* token (`surface.white` or `surface.main`) and provides that tone to descendants via context.
|
|
30
|
+
* Wrap a screen (or any region) in this wherever its background isn't the default white — a page
|
|
31
|
+
* with a muted/grey background, for instance — so anything inside that needs to contrast with its
|
|
32
|
+
* background picks the right fill automatically rather than assuming white.
|
|
33
|
+
*/
|
|
34
|
+
export function Surface({ tone, style, children }: SurfaceProps) {
|
|
35
|
+
return (
|
|
36
|
+
<SurfaceToneContext.Provider value={tone}>
|
|
37
|
+
<View style={[tone === 'white' ? styles.white : styles.muted, style]}>{children}</View>
|
|
38
|
+
</SurfaceToneContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
white: { backgroundColor: DS_SEMANTIC.surface.white },
|
|
44
|
+
muted: { backgroundColor: DS_SEMANTIC.surface.main },
|
|
45
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Switch } from './Switch';
|
|
4
|
+
|
|
5
|
+
function SwitchDemo() {
|
|
6
|
+
const [value, setValue] = useState(true);
|
|
7
|
+
return <Switch value={value} onValueChange={setValue} accessibilityLabel="Notifications" />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function SwitchLabelDemo() {
|
|
11
|
+
const [value, setValue] = useState(true);
|
|
12
|
+
return <Switch value={value} onValueChange={setValue} label="Notifications" />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default defineCatalogPage({
|
|
16
|
+
component: 'Switch',
|
|
17
|
+
group: 'Controls',
|
|
18
|
+
specimenSize: 'compact',
|
|
19
|
+
description: 'A boolean on/off toggle. The thumb slides and the track crossfades colour, sharing SegmentedToggle/UnderlineTabs\' own slide-animation hook for a consistent motion feel.',
|
|
20
|
+
whenToUse: 'A setting that takes effect immediately, no separate save step. For recording a fact a future action (like a form submit) will act on, use Checkbox; for one-of-many exclusive selection, use Radio.',
|
|
21
|
+
a11y: 'Renders a Pressable with accessibilityRole="switch" and accessibilityState.checked — pass accessibilityLabel to say what it controls.',
|
|
22
|
+
variants: {
|
|
23
|
+
items: [{ key: 'default', name: 'Default (tap to toggle)', node: <SwitchDemo /> }],
|
|
24
|
+
},
|
|
25
|
+
states: {
|
|
26
|
+
items: [
|
|
27
|
+
{ key: 'off', name: 'Off', node: <Switch value={false} onValueChange={() => {}} accessibilityLabel="Notifications" /> },
|
|
28
|
+
{ key: 'on', name: 'On', node: <Switch value={true} onValueChange={() => {}} accessibilityLabel="Notifications" /> },
|
|
29
|
+
{ key: 'label', name: 'With label', props: { label: true }, node: <SwitchLabelDemo /> },
|
|
30
|
+
{ key: 'disabled', name: 'Disabled', node: <Switch value={true} onValueChange={() => {}} disabled accessibilityLabel="Notifications" /> },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, Text, View, Animated, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_RADIUS, DS_SHADOW, DS_TYPOGRAPHY, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
|
|
4
|
+
import { useSlideAnim } from '../SegmentedToggle/useSlideAnim';
|
|
5
|
+
import { usePressScale } from '../Radio/usePressScale';
|
|
6
|
+
|
|
7
|
+
export interface SwitchProps {
|
|
8
|
+
/** Whether the switch is on. */
|
|
9
|
+
value: boolean;
|
|
10
|
+
/** Called with the new value on tap. */
|
|
11
|
+
onValueChange: (value: boolean) => void;
|
|
12
|
+
/** Optional inline label rendered to the left of the track, at the same bodyMd size/weight
|
|
13
|
+
* Checkbox/Radio use for their own labels — tapping it toggles the switch too. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Non-interactive, dimmed. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Accessible name. Defaults to `label` when set — pass this separately only when the switch
|
|
18
|
+
* needs a different (or the only) spoken name, e.g. no visible label at all. */
|
|
19
|
+
accessibilityLabel?: string;
|
|
20
|
+
style?: StyleProp<ViewStyle>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TRACK_WIDTH = 51;
|
|
24
|
+
const TRACK_HEIGHT = 31;
|
|
25
|
+
// Sized so (TRACK_HEIGHT - THUMB_SIZE) / 2 lands on 4 — the same total inset SegmentedToggle's own
|
|
26
|
+
// thumb gets (its TRACK_PADDING + THUMB_INSET, both DS_SPACING[100]) — so the two sliding controls'
|
|
27
|
+
// thumbs read with the same amount of breathing room around them.
|
|
28
|
+
const THUMB_SIZE = 23;
|
|
29
|
+
// Horizontal inset for the thumb's off/on positions — matched to the vertical gap the track's own
|
|
30
|
+
// `justifyContent: 'center'` already gives it ((TRACK_HEIGHT - THUMB_SIZE) / 2 = 4), so the padding
|
|
31
|
+
// around the thumb reads as an even ring on every side, not tighter left/right than top/bottom.
|
|
32
|
+
const THUMB_INSET = (TRACK_HEIGHT - THUMB_SIZE) / 2;
|
|
33
|
+
// Pads the tappable area vertically out to the accessibility minimum — same reasoning as Button's
|
|
34
|
+
// own `iconOnlyHitSlop`. TRACK_WIDTH (51) already clears the minimum horizontally, so only the
|
|
35
|
+
// vertical sides need it.
|
|
36
|
+
const HIT_SLOP = {
|
|
37
|
+
top: Math.ceil((DS_A11Y_MIN_TOUCH_TARGET - TRACK_HEIGHT) / 2),
|
|
38
|
+
bottom: Math.ceil((DS_A11Y_MIN_TOUCH_TARGET - TRACK_HEIGHT) / 2),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A boolean on/off toggle. The thumb slides between its two resting positions and the track fades
|
|
43
|
+
* between its off/on colours together, driven by the same shared slide animation SegmentedToggle and
|
|
44
|
+
* UnderlineTabs use (JS-driven here, since the track colour crossfade needs it) for a consistent
|
|
45
|
+
* motion feel across the DS's sliding controls. The thumb also grows slightly while pressed, for
|
|
46
|
+
* tactile feedback before release. Kept on the same JS-driven pipeline as the slide/colour (not
|
|
47
|
+
* `useNativeDriver: true`) — mixing native- and JS-driven animations on the same Animated.View's
|
|
48
|
+
* transform is unreliable in RN, and this component's slide already has to be JS-driven anyway.
|
|
49
|
+
*/
|
|
50
|
+
export function Switch({ value, onValueChange, label, disabled = false, accessibilityLabel, style }: SwitchProps) {
|
|
51
|
+
const anim = useSlideAnim(value ? 1 : 0, false);
|
|
52
|
+
const { pressScale, onPressIn, onPressOut } = usePressScale(disabled);
|
|
53
|
+
const [focused, setFocused] = useState(false);
|
|
54
|
+
|
|
55
|
+
const trackColor = anim.interpolate({
|
|
56
|
+
inputRange: [0, 1],
|
|
57
|
+
outputRange: [DS_SEMANTIC.surface.recessed, DS_SEMANTIC.emphasis.positive],
|
|
58
|
+
});
|
|
59
|
+
const thumbX = anim.interpolate({
|
|
60
|
+
inputRange: [0, 1],
|
|
61
|
+
outputRange: [THUMB_INSET, TRACK_WIDTH - THUMB_SIZE - THUMB_INSET],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Pressable
|
|
66
|
+
onPress={() => !disabled && onValueChange(!value)}
|
|
67
|
+
onPressIn={onPressIn}
|
|
68
|
+
onPressOut={onPressOut}
|
|
69
|
+
onFocus={() => setFocused(true)}
|
|
70
|
+
onBlur={() => setFocused(false)}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
accessibilityRole="switch"
|
|
73
|
+
accessibilityState={{ checked: value, disabled }}
|
|
74
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
75
|
+
hitSlop={HIT_SLOP}
|
|
76
|
+
style={[styles.row, disabled && styles.disabled, style]}
|
|
77
|
+
>
|
|
78
|
+
{/* Label sits before the track (unlike Checkbox/Radio's control-then-label) — matches the
|
|
79
|
+
near-universal settings-row convention (description left, toggle right), while still
|
|
80
|
+
sharing their bodyMd label typography. */}
|
|
81
|
+
{!!label && <Text style={styles.label}>{label}</Text>}
|
|
82
|
+
<Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
|
|
83
|
+
<Animated.View style={[styles.thumb, { transform: [{ translateX: thumbX }, { scale: pressScale }] }]} />
|
|
84
|
+
{/* Keyboard-focus ring — same interaction.focused treatment Button/Pill use; a zero-layout
|
|
85
|
+
absolute overlay so focusing never shifts layout. */}
|
|
86
|
+
{focused && !disabled && <View pointerEvents="none" style={styles.focusRing} />}
|
|
87
|
+
</Animated.View>
|
|
88
|
+
</Pressable>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
row: {
|
|
94
|
+
flexDirection: 'row',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
gap: DS_SPACING[400],
|
|
97
|
+
},
|
|
98
|
+
label: {
|
|
99
|
+
...DS_TYPOGRAPHY.bodyMd,
|
|
100
|
+
color: DS_SEMANTIC.text.regular,
|
|
101
|
+
},
|
|
102
|
+
track: {
|
|
103
|
+
width: TRACK_WIDTH,
|
|
104
|
+
height: TRACK_HEIGHT,
|
|
105
|
+
borderRadius: DS_RADIUS.round,
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
},
|
|
108
|
+
thumb: {
|
|
109
|
+
width: THUMB_SIZE,
|
|
110
|
+
height: THUMB_SIZE,
|
|
111
|
+
borderRadius: DS_RADIUS.round,
|
|
112
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
113
|
+
...DS_SHADOW.resting,
|
|
114
|
+
},
|
|
115
|
+
disabled: {
|
|
116
|
+
opacity: DS_SEMANTIC.interaction.disabledOpacity,
|
|
117
|
+
},
|
|
118
|
+
// 2px air + 2px ring outside the track; fully round to stay concentric.
|
|
119
|
+
focusRing: {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
top: -4,
|
|
122
|
+
bottom: -4,
|
|
123
|
+
left: -4,
|
|
124
|
+
right: -4,
|
|
125
|
+
borderWidth: 2,
|
|
126
|
+
borderColor: DS_SEMANTIC.interaction.focused,
|
|
127
|
+
borderRadius: DS_RADIUS.round,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { TextArea } from './TextArea';
|
|
4
|
+
|
|
5
|
+
function TextAreaDemo() {
|
|
6
|
+
const [value, setValue] = useState('');
|
|
7
|
+
return <TextArea value={value} onChangeText={setValue} placeholder="Describe the issue…" />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default defineCatalogPage({
|
|
11
|
+
component: 'TextArea',
|
|
12
|
+
group: 'Inputs',
|
|
13
|
+
description: 'A multi-line input that grows with its content from a minimum height.',
|
|
14
|
+
a11y: 'A multiline TextInput; pass accessibilityLabel via inputProps when there is no visible label beside it.',
|
|
15
|
+
variants: {
|
|
16
|
+
itemsFill: true,
|
|
17
|
+
items: [{ key: 'default', name: 'Default', node: <TextAreaDemo /> }],
|
|
18
|
+
},
|
|
19
|
+
states: {
|
|
20
|
+
itemsFill: true,
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
key: 'disabled',
|
|
24
|
+
name: 'Disabled',
|
|
25
|
+
node: <TextArea value="This report has already been submitted." onChangeText={() => {}} editable={false} />,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TextInput,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
type TextInputProps,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
10
|
+
import { FieldContainer } from '../FieldContainer';
|
|
11
|
+
|
|
12
|
+
export interface TextAreaProps {
|
|
13
|
+
/** Current text. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** Change handler. */
|
|
16
|
+
onChangeText: (text: string) => void;
|
|
17
|
+
/** Shown when empty. */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Min height of the field before it grows with content (default 96). */
|
|
20
|
+
minHeight?: number;
|
|
21
|
+
editable?: boolean;
|
|
22
|
+
autoFocus?: boolean;
|
|
23
|
+
inputRef?: React.Ref<TextInput>;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
/** Extra TextInput props (e.g. maxLength, accessibilityLabel, onFocus). */
|
|
26
|
+
inputProps?: Omit<
|
|
27
|
+
TextInputProps,
|
|
28
|
+
'value' | 'onChangeText' | 'placeholder' | 'editable' | 'multiline' | 'autoFocus'
|
|
29
|
+
>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A multiline free-text field — built on {@link FieldContainer} (white surface, medium radius, border
|
|
34
|
+
* that darkens on focus) with top-aligned text that grows from `minHeight`. For single-line labelled
|
|
35
|
+
* rows use {@link InputField} — both share the same FieldContainer chrome.
|
|
36
|
+
*/
|
|
37
|
+
export function TextArea({
|
|
38
|
+
value,
|
|
39
|
+
onChangeText,
|
|
40
|
+
placeholder,
|
|
41
|
+
minHeight = 96,
|
|
42
|
+
editable = true,
|
|
43
|
+
autoFocus = false,
|
|
44
|
+
inputRef,
|
|
45
|
+
style,
|
|
46
|
+
inputProps,
|
|
47
|
+
}: TextAreaProps) {
|
|
48
|
+
const [focused, setFocused] = useState(false);
|
|
49
|
+
|
|
50
|
+
const handleFocus = useCallback(
|
|
51
|
+
(e: Parameters<NonNullable<TextInputProps['onFocus']>>[0]) => {
|
|
52
|
+
setFocused(true);
|
|
53
|
+
inputProps?.onFocus?.(e);
|
|
54
|
+
},
|
|
55
|
+
[inputProps],
|
|
56
|
+
);
|
|
57
|
+
const handleBlur = useCallback(
|
|
58
|
+
(e: Parameters<NonNullable<TextInputProps['onBlur']>>[0]) => {
|
|
59
|
+
setFocused(false);
|
|
60
|
+
inputProps?.onBlur?.(e);
|
|
61
|
+
},
|
|
62
|
+
[inputProps],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<FieldContainer focused={focused} disabled={!editable} style={style}>
|
|
67
|
+
<TextInput
|
|
68
|
+
{...inputProps}
|
|
69
|
+
ref={inputRef}
|
|
70
|
+
style={[styles.input, { minHeight }, !editable && styles.inputDisabled]}
|
|
71
|
+
value={value}
|
|
72
|
+
onChangeText={onChangeText}
|
|
73
|
+
placeholder={placeholder}
|
|
74
|
+
placeholderTextColor={editable ? DS_SEMANTIC.text.muted : DS_SEMANTIC.text.disabled}
|
|
75
|
+
editable={editable}
|
|
76
|
+
autoFocus={autoFocus}
|
|
77
|
+
onFocus={handleFocus}
|
|
78
|
+
onBlur={handleBlur}
|
|
79
|
+
multiline
|
|
80
|
+
textAlignVertical="top"
|
|
81
|
+
/>
|
|
82
|
+
</FieldContainer>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
// The chrome (surface/border/radius) lives on FieldContainer; the input owns the text style + padding
|
|
88
|
+
// so a tap anywhere over the padded area still focuses it.
|
|
89
|
+
input: {
|
|
90
|
+
...DS_TYPOGRAPHY.bodySm,
|
|
91
|
+
color: DS_SEMANTIC.text.regular,
|
|
92
|
+
padding: DS_SPACING[400],
|
|
93
|
+
},
|
|
94
|
+
// A step lighter than the hint tone so a non-editable area reads as inactive, not just filled —
|
|
95
|
+
// same convention as InputField/SearchField (see DS_SEMANTIC.text.disabled's own doc comment).
|
|
96
|
+
inputDisabled: {
|
|
97
|
+
color: DS_SEMANTIC.text.disabled,
|
|
98
|
+
},
|
|
99
|
+
});
|