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
2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export PATH="/usr/local/bin:$PATH"

npx lint-staged
6 changes: 6 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const preview: Preview = {
},
},
},
backgrounds: {
options: {
dark: { name: "Dark", value: "#081221" },
light: { name: "Light", value: "#FCFCFD" },
},
},
},
};

Expand Down
4 changes: 2 additions & 2 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Text, View } from "react-native";

import { Button } from "@/components/button";
import { Button } from "@/components/common/Button/Button";

export default function HomeScreen() {
return (
<View className="flex-1 items-center justify-center gap-4 bg-bg">
<View className="flex-1 items-center justify-center gap-4 bg-bg px-4">
<Text className="text-gray-900 text-h-04">비밀번호 찾기</Text>
<Button label="Button" />
</View>
Expand Down
Binary file added assets/images/checktask-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 0 additions & 21 deletions components/button.stories.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions components/button.tsx

This file was deleted.

72 changes: 72 additions & 0 deletions components/common/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
import { View } from "react-native";

import { Button } from "./Button";

const meta = {
title: "Common/Button",
component: Button,
parameters: { layout: "centered" },
tags: ["autodocs"],
argTypes: {
variant: {
control: "inline-radio",
options: ["blue", "gray", "outline", "white"],
},
size: {
control: "inline-radio",
options: ["tiny", "sm", "modal", "lg", "xl"],
},
},
args: {
label: "label",
},
decorators: [
(Story) => (
<View className="w-[358px]">
<Story />
</View>
),
],
} satisfies Meta<typeof Button>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Blue: Story = {};

export const Gray: Story = {
args: { variant: "gray" },
};

export const Outline: Story = {
args: { variant: "outline" },
};

export const White: Story = {
args: { variant: "white", size: "xl" },
};

export const Disabled: Story = {
args: { disabled: true },
};

export const All: Story = {
render: (args) => (
<View className="gap-3">
<Button {...args} size="tiny" variant="outline" className="self-start" />
<View className="flex-row gap-3">
<Button {...args} size="sm" />
<Button {...args} size="sm" variant="outline" />
<Button {...args} size="sm" variant="gray" />
</View>
<Button {...args} size="modal" />
<Button {...args} size="modal" variant="gray" />
<Button {...args} size="lg" />
<Button {...args} size="lg" variant="gray" />
<Button {...args} size="xl" />
<Button {...args} size="xl" variant="white" />
</View>
),
};
79 changes: 79 additions & 0 deletions components/common/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Pressable, Text } from "react-native";

import { cn } from "@/lib/utils";

export type ButtonStyle = "blue" | "gray" | "outline" | "white";
export type ButtonSize = "tiny" | "sm" | "modal" | "lg" | "xl";

const VARIANT_CLASS: Record<ButtonStyle, string> = {
blue: "bg-primary",
gray: "bg-gray-100",
outline: "border border-primary bg-bg",
// white는 xl 전용 색상
white: "bg-gray-0",
};

const VARIANT_TEXT_CLASS: Record<ButtonStyle, string> = {
blue: "text-gray-0",
gray: "text-gray-600",
outline: "text-primary",
white: "text-gray-600",
};

const SIZE_CLASS: Record<ButtonSize, string> = {
tiny: "w-auto rounded-full px-3 py-1.5",
sm: "w-[84px] rounded py-2",
modal: "h-11 rounded-lg p-2.5",
lg: "h-12 rounded-lg p-2.5",
xl: "h-12 p-2.5",
};

const SIZE_TEXT_CLASS: Record<ButtonSize, string> = {
tiny: "text-b-04-m",
sm: "text-b-04-m",
modal: "text-b-02-m",
lg: "text-btn",
xl: "text-btn",
};

export type ButtonProps = {
label: string;
variant?: ButtonStyle;
size?: ButtonSize;
onPress?: () => void;
disabled?: boolean;
className?: string;
};

export function Button({
label,
variant = "blue",
size = "lg",
onPress,
disabled = false,
className,
}: ButtonProps) {
return (
<Pressable
accessibilityRole={onPress ? "button" : undefined}
accessibilityState={{ disabled }}
disabled={disabled}
onPress={onPress}
className={cn(
"w-full items-center justify-center",
SIZE_CLASS[size],
disabled ? "bg-gray-200" : VARIANT_CLASS[variant],
className
)}
>
<Text
className={cn(
disabled ? "text-gray-400" : VARIANT_TEXT_CLASS[variant],
SIZE_TEXT_CLASS[size]
)}
>
{label}
</Text>
</Pressable>
);
}
31 changes: 31 additions & 0 deletions components/common/Fab/Fab.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
import { View } from "react-native";

import { Fab } from "./Fab";

const meta = {
title: "Common/Fab",
component: Fab,
parameters: { layout: "centered" },
tags: ["autodocs"],
argTypes: {
defaultOpen: { control: "boolean" },
},
decorators: [
(Story) => (
<View pointerEvents="none">
<Story />
</View>
),
],
Comment on lines +14 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target file ---'
cat -n components/common/Fab/Fab.stories.tsx
printf '%s\n' '--- related Fab files ---'
fd -i 'Fab' components/common/Fab
printf '%s\n' '--- pointerEvents usage ---'
rg -n --glob '*.tsx' --glob '*.ts' 'pointerEvents|Fab.stories|<Pressable|onPress' components/common/Fab components 2>/dev/null | head -200

Repository: check-task/mobile-frontend

Length of output: 4610


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Fab implementation ---'
cat -n components/common/Fab/Fab.tsx
printf '%s\n' '--- package/runtime context ---'
rg -n '"react-native"|"react-native-web"|"`@storybook/react-native-web-vite`"|storybook' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | head -120
printf '%s\n' '--- Storybook configuration ---'
fd -i 'storybook' . --exclude node_modules --exclude dist --exclude build

Repository: check-task/mobile-frontend

Length of output: 11438


🌐 Web query:

React Native View pointerEvents="none" children touch events react-native-web documentation

💡 Result:

In React Native and React Native for Web, the pointerEvents prop on a View component determines how that view and its children respond to touch events [1][2]. When you set pointerEvents="none" on a View, the View itself becomes transparent to all touch events, and crucially, all of its children are also excluded from receiving touch events [1][3]. If your goal is to make a parent View ignore touch events while allowing its children to remain interactive, you should use pointerEvents="box-none" instead [1][4]. Summary of pointerEvents values: - auto: The View and its children can be targets of touch events (default) [1]. - none: Neither the View nor its children can be targets of touch events [1][3]. - box-none: The View is never the target of touch events, but its subviews (children) can be [1][4]. - box-only: The View can be the target of touch events, but its subviews cannot be [1][3]. This behavior is consistent across both React Native and React Native for Web [2][3][5]. In the context of React Native for Web, these values are implemented by applying specific CSS pointer-events rules to the container and its children [2][6]. Note that pointerEvents is a property of the View component, not a CSS style, and must be passed as a prop [4].

Citations:


pointerEvents="none"을 제거하세요.

이 설정은 Fab 내부의 모든 Pressable 입력을 차단합니다. 제거하면 Storybook에서 FAB 토글과 보조 액션을 사용할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/common/Fab/Fab.stories.tsx` around lines 14 - 20, Remove
pointerEvents="none" from the View wrapper in the decorators configuration so
the Fab Storybook story allows interaction with the FAB toggle and secondary
Pressable actions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보조 액션 없이 그 상태가 보이도록 하려고 추가했습니다

} satisfies Meta<typeof Fab>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Open: Story = {
args: { defaultOpen: true },
};
79 changes: 79 additions & 0 deletions components/common/Fab/Fab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState } from "react";
import { Pressable, Text, View } from "react-native";

import { Add, Cancel, TaskGenerate, TaskJoin } from "@/components/icons";
import { FAB_PRIMARY_SHADOW, FAB_SECONDARY_SHADOW } from "@/constants/shadows";
import { cn } from "@/lib/utils";

export type FabProps = {
onCreateTask?: () => void;
onJoinTeamTask?: () => void;
defaultOpen?: boolean;
className?: string;
};

export function Fab({
onCreateTask,
onJoinTeamTask,
defaultOpen = false,
className,
}: FabProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);

function handleCreateTask() {
setIsOpen(false);
onCreateTask?.();
}

function handleJoinTeamTask() {
setIsOpen(false);
onJoinTeamTask?.();
}

return (
<View className={cn("items-end gap-3", className)}>
{isOpen ? (
<>
<View className="flex-row items-center gap-4">
<Text className="text-gray-0 text-b-01-m">새 과제 생성</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="새 과제 생성"
onPress={handleCreateTask}
style={FAB_SECONDARY_SHADOW}
className="size-[60px] items-center justify-center rounded-full bg-gray-100"
>
<TaskGenerate variant="2xl" className="text-gray-800" />
</Pressable>
</View>
<View className="flex-row items-center gap-4">
<Text className="text-gray-0 text-b-01-m">팀 과제 참여</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="팀 과제 참여"
onPress={handleJoinTeamTask}
style={FAB_SECONDARY_SHADOW}
className="size-[60px] items-center justify-center rounded-full bg-gray-100"
>
<TaskJoin variant="2xl" className="text-gray-800" />
</Pressable>
</View>
</>
) : null}

<Pressable
accessibilityRole="button"
accessibilityLabel={isOpen ? "닫기" : "새 과제 만들기"}
onPress={() => setIsOpen((prev) => !prev)}
style={FAB_PRIMARY_SHADOW}
className="size-[60px] items-center justify-center rounded-full bg-blue-500"
>
{isOpen ? (
<Cancel variant="2xl" className="text-primary-button-text" />
) : (
<Add variant="2xl" className="text-primary-button-text" />
)}
</Pressable>
</View>
);
}
30 changes: 30 additions & 0 deletions components/common/Header/BackHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Pressable, Text } from "react-native";

import { Left } from "@/components/icons";

import { HeaderContainer } from "./HeaderContainer";

export type BackHeaderProps = {
label?: string;
onBack?: () => void;
className?: string;
};

export function BackHeader({ label, onBack, className }: BackHeaderProps) {
return (
<HeaderContainer className={className}>
<Pressable
accessibilityRole={onBack ? "button" : undefined}
accessibilityLabel="뒤로가기"
onPress={onBack}
hitSlop={8}
className="flex-row items-center gap-1"
>
<Left variant="lg" className="text-gray-800" />
{label ? (
<Text className="text-gray-800 text-b-01-m">{label}</Text>
) : null}
</Pressable>
</HeaderContainer>
);
}
26 changes: 26 additions & 0 deletions components/common/Header/CloseHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Pressable } from "react-native";

import { Cancel } from "@/components/icons";

import { HeaderContainer } from "./HeaderContainer";

export type CloseHeaderProps = {
onClose?: () => void;
className?: string;
};

export function CloseHeader({ onClose, className }: CloseHeaderProps) {
return (
<HeaderContainer className={className}>
<Pressable
accessibilityRole={onClose ? "button" : undefined}
accessibilityLabel="닫기"
onPress={onClose}
hitSlop={8}
className="ml-auto"
>
<Cancel variant="xl" color="text-gray-800" />
</Pressable>
</HeaderContainer>
);
}
Loading
Loading