Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions assets/images/clear.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions assets/images/loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/images/loadingbtn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/images/loadingbtn_blue.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/images/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 123 additions & 0 deletions src/app/dev/components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useState } from 'react';
import { Redirect } from 'expo-router';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { ActionButton } from '@/components/ActionButton';
import { Chip } from '@/components/Chip';
import { Loading } from '@/components/Loading';
import { ProgressBar } from '@/components/ProgressBar';
import { SearchBar } from '@/components/SearchBar';
import { SelectButton } from '@/components/SelectButton';
import { TextInput } from '@/components/TextInput';
import { semantic } from '@/styles/colors';
import { gap, grid } from '@/styles/measurement';
import { typography } from '@/styles/typography';

// 개발 중에만 여는 공통 컴포넌트 확인 화면입니다.
export default function ComponentsPreview() {
const [empty, setEmpty] = useState('');
const [filled, setFilled] = useState('값');
const [error, setError] = useState('값');
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState(true);
const [chip, setChip] = useState(false);
const [search, setSearch] = useState('은행을 검색하세요');

if (!__DEV__) return <Redirect href="/" />;

return (
<ScrollView style={s.page} contentContainerStyle={s.content}>
<Text style={s.title}>TextInput</Text>
<TextInput
value={empty}
onChangeText={setEmpty}
placeholder="텍스트를 입력해주세요"
helperText="인풋을 보조하는 헬퍼텍스트"
/>
<TextInput
value={filled}
onChangeText={setFilled}
placeholder="텍스트를 입력해주세요"
helperText="인풋을 보조하는 헬퍼텍스트"
/>
<TextInput
value={error}
onChangeText={setError}
placeholder="텍스트를 입력해주세요"
errorMessage="에러메세지"
/>

<Text style={s.title}>Progress Bar</Text>
<ProgressBar step={1} total={2} showLabel={false} />
{[1, 2, 3, 4, 5].map((step) => (
<ProgressBar key={step} step={step} total={5} />
))}

<Text style={s.title}>Loading</Text>
<Pressable
role="button"
onPress={() => {
setLoading(true);
setTimeout(() => setLoading(false), 3000);
}}
>
<Text style={s.link}>3초 동안 로딩 보기</Text>
</Pressable>
<Loading visible={loading} />

<Text style={s.title}>ActionButton</Text>
{(['S', 'M'] as const).map((size) => (
<View key={size} style={s.row}>
<ActionButton size={size} label="btn" />
<ActionButton size={size} label="btn" disabled />
<ActionButton size={size} label="btn" loading />
</View>
))}
<ActionButton label="btn" />
<ActionButton label="btn" disabled />
<ActionButton label="btn" loading />
{(['S', 'M'] as const).map((size) => (
<View key={size} style={s.row}>
<ActionButton variant="secondary" size={size} label="btn" />
<ActionButton variant="secondary" size={size} label="btn" disabled />
<ActionButton variant="secondary" size={size} label="btn" loading />
</View>
))}
<ActionButton variant="secondary" label="btn" />
<ActionButton variant="secondary" label="btn" disabled />
<ActionButton variant="secondary" label="btn" loading />

<Text style={s.title}>SelectButton</Text>
<SelectButton label="학생 당일 취소" />
<SelectButton
label="학생 당일 취소"
selected={selected}
onPress={() => setSelected((v) => !v)}
/>

<Text style={s.title}>Chip</Text>
<View style={s.row}>
<Chip label="text" />
<Chip label="text" selected />
<Chip label="고2" selected={chip} onPress={() => setChip((v) => !v)} />
</View>

<Text style={s.title}>SearchBar</Text>
<SearchBar placeholder="은행을 검색하세요" />
<SearchBar value={search} onChangeText={setSearch} placeholder="은행을 검색하세요" />
</ScrollView>
);
}

const s = StyleSheet.create({
page: { flex: 1, backgroundColor: semantic.surface.input },
content: {
width: '100%',
maxWidth: 390,
alignSelf: 'center',
padding: grid.margin,
gap: gap[16],
},
title: { ...typography.heading[1].bold, color: semantic.text.default.strong },
link: { ...typography.body[2].medium, color: semantic.text.brand.normal },
row: { flexDirection: 'row', gap: gap[8] },
});
118 changes: 118 additions & 0 deletions src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import loadingDotsImage from '../../assets/images/loadingbtn.svg';
import loadingDotsBlueImage from '../../assets/images/loadingbtn_blue.svg';
import { Pressable, StyleSheet, Text } from 'react-native';
import type { PressableProps } from 'react-native';
import { Image } from 'expo-image';
import { semantic } from '@/styles/colors';
import { padding, radius } from '@/styles/measurement';
import { fontSize, fontWeight, letterSpacing, lineHeight } from '@/styles/typography';

// TODO: 피그마에서 아직 확인하지 않은 값입니다. 확인 후 토큰으로 교체해야 합니다.
const unconfirmed = {
primaryLoadingBackground: semantic.action.pressed,
secondaryLoadingBackground: semantic.surface.default,
};

export type ActionButtonSize = 'L' | 'M' | 'S';
/** primary: 파란 채움, secondary: 연한 배경 */
export type ActionButtonVariant = 'primary' | 'secondary';

export type ActionButtonProps = Omit<PressableProps, 'style' | 'children'> & {
label: string;
/** L: 가로 꽉 참, M: 96, S: 72 */
size?: ActionButtonSize;
variant?: ActionButtonVariant;
/** true면 글씨 대신 로딩 점을 보여 주고 누를 수 없게 합니다. */
loading?: boolean;
};

const variants = {
primary: {
background: semantic.action.default,
pressedBackground: semantic.action.pressed,
loadingBackground: unconfirmed.primaryLoadingBackground,
disabledBackground: semantic.action.disabled,
text: semantic.text.default.inverse,
disabledText: semantic.text.default.weak,
dots: loadingDotsImage,
},
secondary: {
background: semantic.surface.default,
pressedBackground: semantic.surface.default,
loadingBackground: unconfirmed.secondaryLoadingBackground,
disabledBackground: semantic.surface.disabled,
text: semantic.text.brand.assistive,
disabledText: semantic.text.default.weak,
dots: loadingDotsBlueImage,
},
};

export function ActionButton({
label,
size = 'L',
variant = 'primary',
disabled,
loading = false,
...props
}: ActionButtonProps) {
const v = variants[variant];
const background = (pressed: boolean) => {
if (disabled) return v.disabledBackground;
if (loading) return v.loadingBackground;
return pressed ? v.pressedBackground : v.background;
};

return (
<Pressable
{...props}
role="button"
accessibilityLabel={label}
disabled={disabled || loading}
aria-disabled={disabled || loading || undefined}
aria-busy={loading || undefined}
style={({ pressed }) => [s.base, sizes[size], { backgroundColor: background(pressed) }]}
>
{loading ? (
<Image source={v.dots} style={s.dots} loading="eager" />
) : (
<Text style={[s.label, labels[size], { color: disabled ? v.disabledText : v.text }]}>
{label}
</Text>
)}
</Pressable>
);
}

const s = StyleSheet.create({
base: {
alignItems: 'center',
justifyContent: 'center',
borderRadius: radius.small,
},
label: { fontWeight: fontWeight.semiBold, textAlign: 'center' },
dots: { width: 23, height: 6 },
});

const sizes = StyleSheet.create({
L: { width: '100%', height: 56, paddingVertical: padding[16] },
M: { width: 96, height: 42, padding: padding[12] },
S: { width: 72, height: 34, paddingVertical: padding[8], paddingHorizontal: padding[12] },
});

const labels = StyleSheet.create({
L: {
fontSize: fontSize[18],
lineHeight: lineHeight[16],
letterSpacing: letterSpacing.normal,
},
M: {
fontSize: fontSize[14],
lineHeight: lineHeight[13],
letterSpacing: letterSpacing.none,
},
S: {
fontSize: fontSize[13],
lineHeight: lineHeight[13],
letterSpacing: letterSpacing.none,
},
});
62 changes: 62 additions & 0 deletions src/components/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Pressable, StyleSheet, Text } from 'react-native';
import type { PressableProps } from 'react-native';
import { semantic } from '@/styles/colors';
import { lineWeight, padding, radius } from '@/styles/measurement';
import { fontSize, fontWeight, letterSpacing, lineHeight } from '@/styles/typography';

const colors = {
background: semantic.surface.default,
border: semantic.border.default,
text: semantic.text.default.normal,
selectedBackground: semantic.action.default,
selectedBorder: semantic.border.focused,
selectedText: semantic.text.default.inverse,
};

export type ChipProps = Omit<PressableProps, 'style' | 'children'> & {
label: string;
selected?: boolean;
};

/** 둥근 선택 칩. 너비는 글자 길이에 맞춰집니다. */
export function Chip({ label, selected = false, ...props }: ChipProps) {
return (
<Pressable
{...props}
role="checkbox"
aria-checked={selected}
accessibilityLabel={label}
style={[
s.base,
{
backgroundColor: selected ? colors.selectedBackground : colors.background,
borderColor: selected ? colors.selectedBorder : colors.border,
},
]}
>
<Text style={[s.label, { color: selected ? colors.selectedText : colors.text }]}>
{label}
</Text>
</Pressable>
);
}

const s = StyleSheet.create({
base: {
alignSelf: 'flex-start',
height: 34,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: padding[8],
paddingHorizontal: padding[12],
borderRadius: radius.full,
borderWidth: lineWeight.regular,
},
label: {
fontSize: fontSize[14],
lineHeight: lineHeight[13],
fontWeight: fontWeight.medium,
letterSpacing: letterSpacing.none,
textAlign: 'center',
},
});
Loading
Loading