@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,77 @@
|
|
|
1
|
+
import React, { isValidElement, type ReactNode } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
|
|
4
|
+
import { Button, type ButtonProps } from '../Button';
|
|
5
|
+
|
|
6
|
+
// Both slots reserve this width whether populated or empty, so the title stays centered
|
|
7
|
+
// regardless of which side(s) actually have content.
|
|
8
|
+
const SLOT_SIZE = DS_A11Y_MIN_TOUCH_TARGET;
|
|
9
|
+
|
|
10
|
+
/** Dev-time consistency check (same policy as ButtonGroup's `checkConsistency`) — a nav bar's own
|
|
11
|
+
* leading/trailing actions read as one quiet, uniform family (back arrow, close, overflow menu),
|
|
12
|
+
* never as a prominent CTA. Warns (never throws) if a slot's Button doesn't follow that: `small`
|
|
13
|
+
* size, and not `ghost` (which reads as an inline text action, not a nav icon — see Button's own
|
|
14
|
+
* variant-choice doc). */
|
|
15
|
+
function checkSlotButton(node: ReactNode, slotName: 'leading' | 'trailing'): void {
|
|
16
|
+
if (!isValidElement(node) || node.type !== Button) return;
|
|
17
|
+
const props = node.props as ButtonProps;
|
|
18
|
+
if (props.size && props.size !== 'small') {
|
|
19
|
+
console.warn(`[TopNav] ${slotName} button uses size="${props.size}" — TopNav slot buttons should use size="small".`);
|
|
20
|
+
}
|
|
21
|
+
if (props.variant === 'ghost') {
|
|
22
|
+
console.warn(
|
|
23
|
+
`[TopNav] ${slotName} button uses variant="ghost" — that reads as an inline text action, not a nav icon. Use variant="secondary" (TopNav's default) instead.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TopNavProps {
|
|
29
|
+
/** Centered title text. Ignored when `center` is set. */
|
|
30
|
+
title?: string;
|
|
31
|
+
/** Custom content overriding the centered title — e.g. a search field or segmented toggle. */
|
|
32
|
+
center?: ReactNode;
|
|
33
|
+
/** Leading slot — typically a back/close icon Button. Keep it `size="small"` and
|
|
34
|
+
* `variant="secondary"` (TopNav's default) unless there's a specific reason to deviate — never
|
|
35
|
+
* `ghost`, which reads as an inline text action rather than a nav icon. */
|
|
36
|
+
leading?: ReactNode;
|
|
37
|
+
/** Trailing slot — typically an action icon Button. Same size/variant guidance as `leading`. */
|
|
38
|
+
trailing?: ReactNode;
|
|
39
|
+
style?: StyleProp<ViewStyle>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A screen's top bar: fixed-width leading/trailing slots flanking a centered title (or custom
|
|
43
|
+
* `center` content). Slots reserve their layout space even when empty, so the title stays
|
|
44
|
+
* centered whether one, both, or neither side has content. */
|
|
45
|
+
export function TopNav({ title, center, leading, trailing, style }: TopNavProps) {
|
|
46
|
+
checkSlotButton(leading, 'leading');
|
|
47
|
+
checkSlotButton(trailing, 'trailing');
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.row, style]}>
|
|
50
|
+
<View style={styles.slot}>{leading}</View>
|
|
51
|
+
<View style={styles.center}>
|
|
52
|
+
{center ?? (title != null && (
|
|
53
|
+
<Text style={styles.title} numberOfLines={1} accessibilityRole="header">
|
|
54
|
+
{title}
|
|
55
|
+
</Text>
|
|
56
|
+
))}
|
|
57
|
+
</View>
|
|
58
|
+
<View style={[styles.slot, styles.slotTrailing]}>{trailing}</View>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
row: {
|
|
65
|
+
width: '100%',
|
|
66
|
+
flexDirection: 'row',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
paddingHorizontal: DS_SPACING[800],
|
|
69
|
+
paddingTop: DS_SPACING[800],
|
|
70
|
+
paddingBottom: DS_SPACING[400],
|
|
71
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
72
|
+
},
|
|
73
|
+
slot: { width: SLOT_SIZE, height: SLOT_SIZE, alignItems: 'flex-start', justifyContent: 'center' },
|
|
74
|
+
slotTrailing: { alignItems: 'flex-end' },
|
|
75
|
+
center: { flex: 1, alignItems: 'center' },
|
|
76
|
+
title: { ...DS_TYPOGRAPHY.labelMd, color: DS_SEMANTIC.text.regular },
|
|
77
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { UnderlineTabs } from './UnderlineTabs';
|
|
4
|
+
|
|
5
|
+
function UnderlineTabsDemo() {
|
|
6
|
+
const [value, setValue] = useState('all');
|
|
7
|
+
return (
|
|
8
|
+
<UnderlineTabs
|
|
9
|
+
value={value}
|
|
10
|
+
onChange={setValue}
|
|
11
|
+
options={[
|
|
12
|
+
{ value: 'all', label: 'All' },
|
|
13
|
+
// Leading-icon tab — the layout every other option (icon-less) would otherwise hide.
|
|
14
|
+
{ value: 'nearby', label: 'Nearby', iconName: 'pin' },
|
|
15
|
+
{ value: 'favorites', label: 'Favorites', badge: 2 },
|
|
16
|
+
]}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default defineCatalogPage({
|
|
22
|
+
component: 'UnderlineTabs',
|
|
23
|
+
group: 'Selection',
|
|
24
|
+
// Phone width plus a small phone, where long labels truncate.
|
|
25
|
+
previewWidths: [402, 320],
|
|
26
|
+
description: 'A quieter tab switcher — left-aligned labels over a hairline rule, with a sliding underline indicator. Same options/value/onChange API as SegmentedToggle.',
|
|
27
|
+
whenToUse: "Quiet, secondary navigation within a screen that already has a clear primary focus. For a prominent, primary choice, use SegmentedToggle.",
|
|
28
|
+
a11y: 'Each tab is a Pressable label; the underline is a visual indicator only, so selection is also conveyed by the active label weight.',
|
|
29
|
+
render: () => <UnderlineTabsDemo />,
|
|
30
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, Pressable, Animated, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_RADIUS, DS_TYPOGRAPHY, DS_ICON_SIZE, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import type { IconName } from '../../icons';
|
|
6
|
+
import { useSlideAnim } from '../SegmentedToggle/useSlideAnim';
|
|
7
|
+
|
|
8
|
+
export interface UnderlineTabOption {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
/** Optional leading icon (16px) shown before the label. */
|
|
12
|
+
iconName?: IconName;
|
|
13
|
+
/** Optional count shown in a circular badge after the label (hidden when 0/undefined). */
|
|
14
|
+
badge?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UnderlineTabsProps {
|
|
18
|
+
/** The tabs. */
|
|
19
|
+
options: UnderlineTabOption[];
|
|
20
|
+
/** The currently-selected option `value`. */
|
|
21
|
+
value: string;
|
|
22
|
+
/** Selection handler. */
|
|
23
|
+
onChange: (value: string) => void;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const INDICATOR_HEIGHT = 2;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Low-prominence tab switcher — left-aligned text labels over a hairline rule, with a thin underline
|
|
31
|
+
* indicator that slides to the selected tab. A quieter stand-in for SegmentedToggle: same
|
|
32
|
+
* options/value/onChange API, but no filled track or lifted thumb. Two or more options.
|
|
33
|
+
*/
|
|
34
|
+
export function UnderlineTabs({ options, value, onChange, style }: UnderlineTabsProps) {
|
|
35
|
+
// -1 when `value` matches no option (e.g. stale state after the options list changed). The
|
|
36
|
+
// indicator is hidden entirely in that case — clamping to tab 0 would underline a tab whose own
|
|
37
|
+
// label doesn't render as selected, a visibly contradictory state.
|
|
38
|
+
const matchedIndex = options.findIndex(o => o.value === value);
|
|
39
|
+
const hasSelection = matchedIndex !== -1;
|
|
40
|
+
const selectedIndex = Math.max(0, matchedIndex);
|
|
41
|
+
// Per-tab measured geometry (x offset + width within the row), so the indicator can size and slide to
|
|
42
|
+
// each variable-width label.
|
|
43
|
+
const [layouts, setLayouts] = useState<Record<number, { x: number; width: number }>>({});
|
|
44
|
+
// Which tab currently holds keyboard focus (null = none).
|
|
45
|
+
const [focusedValue, setFocusedValue] = useState<string | null>(null);
|
|
46
|
+
// translateX + width animate together; width can't use the native driver, so neither does.
|
|
47
|
+
const anim = useSlideAnim(selectedIndex, false);
|
|
48
|
+
const measured = options.every((_, i) => layouts[i] != null);
|
|
49
|
+
|
|
50
|
+
// Unlike SegmentedToggle's analytically-computed thumb position, this indicator slides to each
|
|
51
|
+
// tab's real measured `x` (from onLayout) — already RTL-correct as-is, since onLayout reports actual
|
|
52
|
+
// post-mirroring screen position, not an index-based LTR assumption.
|
|
53
|
+
const indices = options.map((_, i) => i);
|
|
54
|
+
const indicatorX = measured
|
|
55
|
+
? anim.interpolate({ inputRange: indices, outputRange: indices.map(i => layouts[i].x) })
|
|
56
|
+
: 0;
|
|
57
|
+
const indicatorW = measured
|
|
58
|
+
? anim.interpolate({ inputRange: indices, outputRange: indices.map(i => layouts[i].width) })
|
|
59
|
+
: 0;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View style={[styles.row, style]} accessibilityRole="tablist">
|
|
63
|
+
{options.map((opt, i) => {
|
|
64
|
+
const selected = opt.value === value;
|
|
65
|
+
const color = selected ? DS_SEMANTIC.text.regular : DS_SEMANTIC.text.muted;
|
|
66
|
+
return (
|
|
67
|
+
<Pressable
|
|
68
|
+
key={opt.value}
|
|
69
|
+
onPress={() => onChange(opt.value)}
|
|
70
|
+
onLayout={e => {
|
|
71
|
+
const { x, width } = e.nativeEvent.layout;
|
|
72
|
+
setLayouts(prev =>
|
|
73
|
+
prev[i] && prev[i].x === x && prev[i].width === width ? prev : { ...prev, [i]: { x, width } },
|
|
74
|
+
);
|
|
75
|
+
}}
|
|
76
|
+
onFocus={() => setFocusedValue(opt.value)}
|
|
77
|
+
onBlur={() => setFocusedValue(prev => (prev === opt.value ? null : prev))}
|
|
78
|
+
accessibilityRole="tab"
|
|
79
|
+
accessibilityLabel={opt.badge != null && opt.badge > 0 ? `${opt.label}, ${opt.badge}` : opt.label}
|
|
80
|
+
accessibilityState={{ selected }}
|
|
81
|
+
// Keyboard focus shows the same highlight as a press (same policy as SegmentedToggle/ListItem).
|
|
82
|
+
style={({ pressed }) => [styles.tab, (pressed || focusedValue === opt.value) && styles.tabPressed]}
|
|
83
|
+
>
|
|
84
|
+
{opt.iconName && <Icon name={opt.iconName} size={DS_ICON_SIZE.sm} color={color} />}
|
|
85
|
+
<Text style={[styles.label, { color }]}>{opt.label}</Text>
|
|
86
|
+
{opt.badge != null && opt.badge > 0 && (
|
|
87
|
+
<View style={[styles.badge, { backgroundColor: color }]}>
|
|
88
|
+
<Text style={[styles.badgeText, { color: DS_SEMANTIC.surface.white }]}>{opt.badge}</Text>
|
|
89
|
+
</View>
|
|
90
|
+
)}
|
|
91
|
+
</Pressable>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
{measured && hasSelection && (
|
|
95
|
+
<Animated.View
|
|
96
|
+
pointerEvents="none"
|
|
97
|
+
style={[styles.indicator, { width: indicatorW, transform: [{ translateX: indicatorX }] }]}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const styles = StyleSheet.create({
|
|
105
|
+
row: {
|
|
106
|
+
flexDirection: 'row',
|
|
107
|
+
alignItems: 'flex-end',
|
|
108
|
+
gap: DS_SPACING[800],
|
|
109
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
110
|
+
borderBottomColor: DS_SEMANTIC.element.divider,
|
|
111
|
+
},
|
|
112
|
+
tab: {
|
|
113
|
+
flexDirection: 'row',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: DS_SPACING[200],
|
|
116
|
+
paddingTop: DS_SPACING[400],
|
|
117
|
+
paddingBottom: DS_SPACING[600],
|
|
118
|
+
// A fixed floor (not a spacing token — this is an accessibility guideline number, not a design
|
|
119
|
+
// choice) so the tab's compact visual padding still guarantees a touch target.
|
|
120
|
+
minHeight: DS_A11Y_MIN_TOUCH_TARGET,
|
|
121
|
+
},
|
|
122
|
+
tabPressed: {
|
|
123
|
+
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
124
|
+
borderRadius: DS_RADIUS.small,
|
|
125
|
+
},
|
|
126
|
+
label: {
|
|
127
|
+
// Matches the section-header label size (labelXs) it stands in for.
|
|
128
|
+
...DS_TYPOGRAPHY.labelXs,
|
|
129
|
+
// Pin the line box to the badge height (18) so a tab is the same height with or without a count
|
|
130
|
+
// badge — otherwise the taller badged tab grows the row and shifts the divider/content below it.
|
|
131
|
+
lineHeight: 18,
|
|
132
|
+
},
|
|
133
|
+
// The sliding underline, sat on the hairline rule beneath the selected tab.
|
|
134
|
+
indicator: {
|
|
135
|
+
position: 'absolute',
|
|
136
|
+
left: 0,
|
|
137
|
+
bottom: -StyleSheet.hairlineWidth,
|
|
138
|
+
height: INDICATOR_HEIGHT,
|
|
139
|
+
borderRadius: INDICATOR_HEIGHT / 2,
|
|
140
|
+
backgroundColor: DS_SEMANTIC.text.regular,
|
|
141
|
+
},
|
|
142
|
+
// Fixed-diameter circle so the count badge stays round. 18 doesn't match a DS_SPACING/DS_ICON_SIZE
|
|
143
|
+
// step (16/20 are the neighbors) — sized to comfortably fit a 1-2 digit count at labelXs.
|
|
144
|
+
badge: {
|
|
145
|
+
width: 18,
|
|
146
|
+
height: 18,
|
|
147
|
+
borderRadius: DS_RADIUS.round,
|
|
148
|
+
alignItems: 'center',
|
|
149
|
+
justifyContent: 'center',
|
|
150
|
+
},
|
|
151
|
+
badgeText: {
|
|
152
|
+
...DS_TYPOGRAPHY.labelXs,
|
|
153
|
+
// Pinned to the badge's own 18px diameter (above) so the count sits centered in the circle.
|
|
154
|
+
lineHeight: 18,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Barrel for the React Native component tree. */
|
|
2
|
+
export * from './AnimatedChevron';
|
|
3
|
+
export * from './Avatar';
|
|
4
|
+
export * from './Badge';
|
|
5
|
+
export * from './Banner';
|
|
6
|
+
export * from './BottomSheet';
|
|
7
|
+
export * from './Button';
|
|
8
|
+
export * from './ButtonGroup';
|
|
9
|
+
export * from './Card';
|
|
10
|
+
export * from './Checkbox';
|
|
11
|
+
export * from './Dialog';
|
|
12
|
+
export * from './Divider';
|
|
13
|
+
export * from './Dock';
|
|
14
|
+
export * from './Dropdown';
|
|
15
|
+
export * from './EmptyState';
|
|
16
|
+
export * from './FieldContainer';
|
|
17
|
+
export * from './InputClearButton';
|
|
18
|
+
export * from './InputField';
|
|
19
|
+
export * from './List';
|
|
20
|
+
export * from './ListItem';
|
|
21
|
+
export * from './Loading';
|
|
22
|
+
export * from './Pill';
|
|
23
|
+
export * from './PillRow';
|
|
24
|
+
export * from './ProgressDots';
|
|
25
|
+
export * from './Radio';
|
|
26
|
+
export * from './SearchField';
|
|
27
|
+
export * from './SectionHeader';
|
|
28
|
+
export * from './SegmentedToggle';
|
|
29
|
+
export * from './Shimmer';
|
|
30
|
+
export * from './Surface';
|
|
31
|
+
export * from './Switch';
|
|
32
|
+
export * from './TextArea';
|
|
33
|
+
export * from './Toast';
|
|
34
|
+
export * from './Tooltip';
|
|
35
|
+
export * from './TopNav';
|
|
36
|
+
export * from './UnderlineTabs';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DS_SEMANTIC } from '../tokens';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one shared status→color lookup behind every status-tinted component (Badge, Banner): a light
|
|
5
|
+
* tinted fill (`shade`) paired with a saturated foreground (`emphasis`), so a badge and a banner of
|
|
6
|
+
* the same status always read as the same color identity. Previously each component maintained its
|
|
7
|
+
* own identical copy of this map — one place to change now keeps them matched by construction.
|
|
8
|
+
* (Toast's variant map lives separately: its variant names differ (`success`/`informational`) and it
|
|
9
|
+
* bundles per-variant icons, so it's a different shape, not a third copy of this one.)
|
|
10
|
+
*/
|
|
11
|
+
export type StatusVariant = 'neutral' | 'info' | 'positive' | 'warning' | 'negative';
|
|
12
|
+
|
|
13
|
+
export const STATUS_BG: Record<StatusVariant, string> = {
|
|
14
|
+
neutral: DS_SEMANTIC.shade.neutral,
|
|
15
|
+
info: DS_SEMANTIC.shade.info,
|
|
16
|
+
positive: DS_SEMANTIC.shade.positive,
|
|
17
|
+
warning: DS_SEMANTIC.shade.warning,
|
|
18
|
+
negative: DS_SEMANTIC.shade.negative,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const STATUS_FG: Record<StatusVariant, string> = {
|
|
22
|
+
neutral: DS_SEMANTIC.emphasis.neutral,
|
|
23
|
+
info: DS_SEMANTIC.emphasis.info,
|
|
24
|
+
positive: DS_SEMANTIC.emphasis.positive,
|
|
25
|
+
warning: DS_SEMANTIC.emphasis.warning,
|
|
26
|
+
negative: DS_SEMANTIC.emphasis.negative,
|
|
27
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-native-svg renderer for the shared icon data (`./paths`).
|
|
3
|
+
*
|
|
4
|
+
* Reads `ICON_PATHS[name]` and maps each primitive to the matching RN-SVG element. Stroke icons
|
|
5
|
+
* get round caps/joins in the Lucide style; fill icons render solid. Per-primitive `fill`/`stroke`
|
|
6
|
+
* overrides (used by the transit icons) win over the def-level mode.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import Svg, { Path, Rect, Circle, Line, Polyline, Polygon } from 'react-native-svg';
|
|
10
|
+
import { DS_SEMANTIC, DS_ICON_SIZE } from '../tokens';
|
|
11
|
+
import { ICON_PATHS, type IconPrimitive } from './paths';
|
|
12
|
+
import type { IconRenderProps } from './types';
|
|
13
|
+
|
|
14
|
+
export type IconProps = IconRenderProps & { accessibilityLabel?: string };
|
|
15
|
+
|
|
16
|
+
/** Resolve a primitive's `'color' | 'none'` sentinel against the active icon color. */
|
|
17
|
+
function resolvePaint(value: 'color' | 'none' | undefined, color: string): string | undefined {
|
|
18
|
+
if (value === undefined) return undefined;
|
|
19
|
+
return value === 'color' ? color : 'none';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Icon({
|
|
23
|
+
name,
|
|
24
|
+
size = DS_ICON_SIZE.sm,
|
|
25
|
+
color = DS_SEMANTIC.text.regular,
|
|
26
|
+
strokeWidth,
|
|
27
|
+
accessibilityLabel,
|
|
28
|
+
}: IconProps) {
|
|
29
|
+
const def = ICON_PATHS[name];
|
|
30
|
+
if (!def) return null;
|
|
31
|
+
|
|
32
|
+
const isStroke = def.mode === 'stroke';
|
|
33
|
+
const defStrokeWidth = strokeWidth ?? def.strokeWidth ?? 2;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Svg
|
|
37
|
+
width={size}
|
|
38
|
+
height={size}
|
|
39
|
+
viewBox={def.viewBox}
|
|
40
|
+
fill="none"
|
|
41
|
+
accessibilityLabel={accessibilityLabel}
|
|
42
|
+
>
|
|
43
|
+
{def.primitives.map((p, i) => {
|
|
44
|
+
// Per-primitive override wins; otherwise fall back to the def mode.
|
|
45
|
+
const fill = resolvePaint(p.fill, color) ?? (isStroke ? 'none' : color);
|
|
46
|
+
const stroke = resolvePaint(p.stroke, color) ?? (isStroke ? color : undefined);
|
|
47
|
+
const hasStroke = stroke !== undefined && stroke !== 'none';
|
|
48
|
+
|
|
49
|
+
const common: Record<string, unknown> = { fill, fillRule: p.fillRule };
|
|
50
|
+
if (hasStroke) {
|
|
51
|
+
common.stroke = stroke;
|
|
52
|
+
common.strokeWidth = p.strokeWidth ?? defStrokeWidth;
|
|
53
|
+
// Round caps/joins are the Lucide house style; only apply for stroke-mode icons.
|
|
54
|
+
if (isStroke) {
|
|
55
|
+
common.strokeLinecap = 'round';
|
|
56
|
+
common.strokeLinejoin = 'round';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// A `transform` string (not `translateX`/`translateY` props) — react-native-svg's web
|
|
60
|
+
// shim only folds `translateX`/`translateY` into the SVG `transform` attribute, it does not
|
|
61
|
+
// strip the original props first, so they would otherwise also reach the DOM unchanged and
|
|
62
|
+
// React would warn "does not recognize the `translateX` prop on a DOM element".
|
|
63
|
+
if (p.translateX !== undefined || p.translateY !== undefined) {
|
|
64
|
+
common.transform = `translate(${p.translateX ?? 0}, ${p.translateY ?? 0})`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return renderPrimitive(p, common, i);
|
|
68
|
+
})}
|
|
69
|
+
</Svg>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderPrimitive(p: IconPrimitive, common: Record<string, unknown>, key: number): React.ReactElement | null {
|
|
74
|
+
switch (p.tag) {
|
|
75
|
+
case 'path':
|
|
76
|
+
return <Path key={key} {...common} d={p.d} />;
|
|
77
|
+
case 'rect':
|
|
78
|
+
return <Rect key={key} {...common} x={p.x} y={p.y} width={p.width} height={p.height} rx={p.rx} ry={p.ry} />;
|
|
79
|
+
case 'circle':
|
|
80
|
+
return <Circle key={key} {...common} cx={p.cx} cy={p.cy} r={p.r} />;
|
|
81
|
+
case 'line':
|
|
82
|
+
return <Line key={key} {...common} x1={p.x1} y1={p.y1} x2={p.x2} y2={p.y2} />;
|
|
83
|
+
case 'polyline':
|
|
84
|
+
return <Polyline key={key} {...common} points={p.points} />;
|
|
85
|
+
case 'polygon':
|
|
86
|
+
return <Polygon key={key} {...common} points={p.points} />;
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared icon layer barrel.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the platform-neutral data + types only. Consumers import the renderer explicitly as
|
|
5
|
+
* `../icons/Icon.native` (the only renderer today — see the repo README for how a future platform
|
|
6
|
+
* port would add its own `Icon.<platform>.tsx` alongside it without touching this data).
|
|
7
|
+
*/
|
|
8
|
+
export { ICON_PATHS } from './paths';
|
|
9
|
+
export type { IconPrimitive, IconDef } from './paths';
|
|
10
|
+
export type { IconName, IconRenderProps } from './types';
|