diff --git a/esbuild.config.mjs b/esbuild.config.mjs index b13282b..96f0885 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,48 +1,79 @@ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; +import fs from "fs"; -const banner = -`/* +const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; -const prod = (process.argv[2] === "production"); +const prod = process.argv[2] === "production"; + +// CSS를 하나로 합치기 위한 rename 플러그인 +const renamePlugin = { + name: 'rename-styles', + setup(build) { + build.onEnd(() => { + const { outfile } = build.initialOptions; + const outcss = outfile.replace(/\.js$/, '.css'); // main.js -> main.css + const fixcss = outfile.replace(/main\.js$/, 'styles.css'); // main.css -> styles.css + + if(fs.existsSync(outcss)) { + console.log('Start renaming', outcss, 'to', fixcss); + fs.renameSync(outcss, fixcss); // main.css -> styles.css + console.log('End renaming', outcss, 'to', fixcss); + } else { + console.log('CSS file not found:', outcss); // 문제 발생 시 경로 확인용 + } + }); + }, +}; const context = await esbuild.context({ - banner: { - js: banner, - }, - entryPoints: ["main.ts"], - bundle: true, - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins], - format: "cjs", - target: "es2018", - logLevel: "info", - sourcemap: prod ? false : "inline", - treeShaking: true, - outfile: "main.js", + banner: { + js: banner, + }, + entryPoints: ["src/main.ts"], // 엔트리 포인트 + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", // CommonJS 포맷 + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", // JavaScript 번들 파일 + plugins: [ + renamePlugin, // CSS 파일 이름 변경 플러그인 + ], + loader: { + ".tsx": "tsx", // TypeScript와 TSX 파일을 처리 + ".ts": "ts", + ".css": "css", // CSS 파일을 로드 + }, + // CSS 파일을 추출하여 하나로 합침 + write: true, }); -if (prod) { - await context.rebuild(); - process.exit(0); +if(prod) { + await context.rebuild(); + process.exit(0); } else { - await context.watch(); -} \ No newline at end of file + await context.watch(); +} diff --git a/src/dev-only-test-btn.ts b/src/dev-only-test-btn.ts index c9c5fd3..70526b1 100644 --- a/src/dev-only-test-btn.ts +++ b/src/dev-only-test-btn.ts @@ -10,7 +10,7 @@ export const devOnlyTest = () => { const test = () => { - routineManager.editRoutine('🖊️ 문서 작성하기', { + routineManager.edit('🖊️ 문서 작성하기', { name: '🖊️ 문서 작성하기ㅋ', newProperties: { dayOfWeeks: [1, 2, 3, 4, 5, 6, 0] diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..3255cfe --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1 @@ +declare module '*.css'; \ No newline at end of file diff --git a/src/lib/day.ts b/src/lib/day.ts new file mode 100644 index 0000000..3406078 --- /dev/null +++ b/src/lib/day.ts @@ -0,0 +1,84 @@ +import { moment } from "obsidian"; +import { plugin } from "./plugin-service-locator"; +import { DEFAULT_SETTINGS } from "settings/DailyRoutineSettingTab"; + + +export enum DayOfWeek { + SUN, + MON, + TUE, + WEN, + THU, + FRI, + SAT +} + +export class Day { + moment: moment.Moment; + static defaultFormat = 'YYYY-MM-DD'; + + /** + * + * @param day "YYYY-MM-DD" 형식의 문자열 + */ + constructor(day: string) { + const m = moment(day); + if(m.isValid()) { + this.moment = m; + } else { + throw new Error(`Invalid date format. ${day}`); + } + } + + static fromNow(): Day{ + return new Day(moment().format(Day.defaultFormat)); + } + + getAsUserCustomFormat(){ + return this.moment.format(gerFormat()); + } + + getAsDefaultFormat(){ + return this.moment.format(Day.defaultFormat); + } + + getDayOfWeek(): DayOfWeek { + const dayOfWeekNum = Number(moment().format('d')); + switch(dayOfWeekNum) { + case 0: return DayOfWeek.SUN; + case 1: return DayOfWeek.MON; + case 2: return DayOfWeek.TUE; + case 3: return DayOfWeek.WEN; + case 4: return DayOfWeek.THU; + case 5: return DayOfWeek.FRI; + case 6: return DayOfWeek.SAT; + default: throw new Error('Invalid day of week number.'); + } + } + + isSameDay(day: Day){ + return this.moment.isSame(day.moment); + } + + isSameDayOfWeek(day: Day){ + return this.getDayOfWeek() === day.getDayOfWeek(); + } + + isSameDayOfWeeks(dayOfWeek: DayOfWeek){ + return this.getDayOfWeek() === dayOfWeek; + } + +} + + +/** + * 사용자가 설정한 날짜 포맷을 가져온다. + */ +const gerFormat = (): string => { + const format = plugin().settings.dateFormat; + if(format && format !== '') { + return format; + } else { + return DEFAULT_SETTINGS.dateFormat as string; + } +} \ No newline at end of file diff --git a/src/shared/file/file-accessor.ts b/src/lib/file-accessor.ts similarity index 96% rename from src/shared/file/file-accessor.ts rename to src/lib/file-accessor.ts index 51c46e1..84c02a0 100644 --- a/src/shared/file/file-accessor.ts +++ b/src/lib/file-accessor.ts @@ -1,5 +1,5 @@ import { TFile, TFolder } from "obsidian"; -import { plugin } from "src/shared/utils/plugin-service-locator"; +import { plugin } from "lib/plugin-service-locator"; diff --git a/src/shared/utils/plugin-service-locator.ts b/src/lib/plugin-service-locator.ts similarity index 100% rename from src/shared/utils/plugin-service-locator.ts rename to src/lib/plugin-service-locator.ts diff --git a/src/shared/utils/mode-switch.ts b/src/lib/utils/mode-switch.ts similarity index 100% rename from src/shared/utils/mode-switch.ts rename to src/lib/utils/mode-switch.ts diff --git a/src/shared/utils/surround-processor.ts b/src/lib/utils/surround-processor.ts similarity index 100% rename from src/shared/utils/surround-processor.ts rename to src/lib/utils/surround-processor.ts diff --git a/src/shared/utils/utils.ts b/src/lib/utils/utils.ts similarity index 94% rename from src/shared/utils/utils.ts rename to src/lib/utils/utils.ts index e19d46d..41a6359 100644 --- a/src/shared/utils/utils.ts +++ b/src/lib/utils/utils.ts @@ -1,5 +1,5 @@ import { Editor, MarkdownView } from "obsidian"; -import { plugin } from "./plugin-service-locator" +import { plugin } from "../plugin-service-locator" diff --git a/src/shared/view/activate-view.ts b/src/lib/view/activate-view.ts similarity index 91% rename from src/shared/view/activate-view.ts rename to src/lib/view/activate-view.ts index 085ac66..b564b81 100644 --- a/src/shared/view/activate-view.ts +++ b/src/lib/view/activate-view.ts @@ -1,5 +1,5 @@ import { WorkspaceLeaf } from "obsidian"; -import { plugin } from "src/shared/utils/plugin-service-locator"; +import { plugin } from "lib/plugin-service-locator"; /** * viewType에 해당하는 뷰를 활성화한다. diff --git a/src/shared/view/react-view.tsx b/src/lib/view/react-view.tsx similarity index 100% rename from src/shared/view/react-view.tsx rename to src/lib/view/react-view.tsx diff --git a/main.ts b/src/main.ts similarity index 74% rename from main.ts rename to src/main.ts index e16eaa0..f1f2900 100644 --- a/main.ts +++ b/src/main.ts @@ -1,9 +1,9 @@ import { Plugin } from 'obsidian'; -import { setPlugin } from 'src/shared/utils/plugin-service-locator'; -import { DailyRoutinePluginSettings, DailyRoutineSettingTab, DEFAULT_SETTINGS } from 'src/settings/DailyRoutineSettingTab'; -import { DailyRoutineView } from 'src/view/daily-routine-view'; -import { activateView } from 'src/shared/view/activate-view'; -import { devOnlyTest } from 'src/dev-only-test-btn'; +import { setPlugin } from './lib/plugin-service-locator'; +import { DailyRoutinePluginSettings, DailyRoutineSettingTab, DEFAULT_SETTINGS } from './settings/DailyRoutineSettingTab'; +import { DailyRoutineView } from './view/daily-routine-view'; +import { activateView } from './lib/view/activate-view'; +import { devOnlyTest } from './dev-only-test-btn'; export default class DailyRoutinePlugin extends Plugin { settings: DailyRoutinePluginSettings; diff --git a/src/model/routine-note.ts b/src/model/routine-note.ts index ac5db32..cbdd25c 100644 --- a/src/model/routine-note.ts +++ b/src/model/routine-note.ts @@ -1,21 +1,25 @@ import { routineManager, Routine } from "./routine" -import { momentProvider } from "src/shared/utils/moment-provider"; +import { Day } from "lib/day"; export interface RoutineNote { title: string; + day: Day; routines: Routine[]; } export const createNewRoutineNote = async (): Promise => { - const now = momentProvider.getNow(); + const day = Day.fromNow(); const routines = await routineManager.getAllRoutines(); - const todayRoutines = routines.filter(r => r.properties.dayOfWeeks.contains(momentProvider.getDayOfWeekNum())); + const todayRoutines = routines.filter(r => { + return r.properties.dayOfWeeks.contains(day.getDayOfWeek()); + }); return { - title: now, + title: day.getAsUserCustomFormat(), + day: day, routines: todayRoutines } } \ No newline at end of file diff --git a/src/model/routine.ts b/src/model/routine.ts index 714a3f0..cb43881 100644 --- a/src/model/routine.ts +++ b/src/model/routine.ts @@ -1,7 +1,8 @@ import { TFile } from "obsidian"; -import { fileAccessor } from "src/shared/file/file-accessor" -import { plugin } from "src/shared/utils/plugin-service-locator"; -import { DayOfWeek } from "src/shared/utils/moment-provider"; +import { fileAccessor } from "lib/file-accessor" +import { plugin } from "lib/plugin-service-locator"; +import { Day, DayOfWeek } from "lib/day"; +import { update } from "lodash"; /** @@ -41,7 +42,7 @@ export const routineManager = { * @param routineName 루틴파일명: identifier * @param cmd 수정할 내용 */ - editRoutine: async (routineName: string, cmd: { + edit: async (routineName: string, cmd: { name?: string, newProperties?: Partial }) => { @@ -68,14 +69,84 @@ export const routineManager = { * 루틴 가져오기 * @param routineName 루틴파일명: identifier */ - getRoutine: async (routineName: string): Promise => { + get: async (routineName: string): Promise => { const path = getRoutinePath(routineName); const file = fileAccessor.getFile(path); return await readRoutineFile(file); }, + /** + * 루틴 성취 업데이트 + */ + updateAchievement: async ({routineName, day, checked}: { + routineName: string, + day: Day, + checked: boolean + }) => { + const file = fileAccessor.getFile(getRoutinePath(routineName)); + const content = await fileAccessor.readFileAsWritable(file); + if(!content){ + throw new Error('Routine file is empty.'); + } + updateAchievement(file, {date: day.getAsDefaultFormat(), checked}); + } + + } + + +interface Achievement { + date: string; // YYYY-MM-DD + checked: boolean; +} +/** + * day에 따른 routine의 성취 로그를 추출해낸다. + */ +const getAchievements = (content: string): Achievement[] => { + // # Achievement 섹션을 추출 + const achievementsRegex = /# Achievement\n((?:- \[(?: |x)\] \d{4}-\d{1,2}-\d{1,2}\n?)+)/; + const achievementsString = content.match(achievementsRegex)?.[0]??''; + if(!achievementsString || achievementsString.length === 0) { + throw new Error("Routine file does not have 'Achievement' section."); + } + + // - [ |x] YYYY-MM-DD 형식의 체크박스를 추출 + const checkboxRegex = /- \[( |x)\] \d{4}-\d{1,2}-\d{1,2}/i; + const achievements = achievementsString.match(checkboxRegex)??[]; + + return achievements.map(a => { + const date = a.match(/\d{4}-\d{1,2}-\d{1,2}/)?.[0]; + if(!date) { + throw new Error('Date format is invalid.'); + } + const checked = a.includes('[X]') || a.includes('[x]'); + return {date, checked}; + }); +} + +/** + * 성취 로그를 업데이트한다. + * @param content 루틴 파일 내용 + * @param cmd 업데이트할 성취 로그 + * 만약 해당 achievement가 없다면 추가하고, 있다면 업데이트한다. + */ +const updateAchievement = (file: TFile, cmd: Achievement) => { + const newAchievements = `- [${cmd.checked?'x':' '}] ${cmd.date}`; + const targetRegex = new RegExp(`- \\[(?: |x)\\] ${cmd.date}`); + + fileAccessor.writeFile(file, (content => { + // 이미 해당 날짜의 성취로그가 있으면 + if(content.match(targetRegex)){ + return content.replace(targetRegex, newAchievements); + } else { + return content.replace(/# Achievement/i, `# Achievement\n${newAchievements}`); + } + })); +} + + + /** * 루틴 이름으로부터 루틴 파일의 경로를 가져온다. */ diff --git a/src/settings/DailyRoutineSettingTab.ts b/src/settings/DailyRoutineSettingTab.ts index 827a939..09fe9b6 100644 --- a/src/settings/DailyRoutineSettingTab.ts +++ b/src/settings/DailyRoutineSettingTab.ts @@ -1,7 +1,7 @@ import DailyRoutinePlugin from "main"; import { App, normalizePath, PluginSettingTab, Setting } from "obsidian"; import { FileSuggest } from "./suggesters/FileSuggester"; -import { momentProvider } from "src/shared/utils/moment-provider"; +import { Day } from "lib/day"; export interface DailyRoutinePluginSettings { @@ -50,7 +50,7 @@ export class DailyRoutineSettingTab extends PluginSettingTab { fragment.append("The date format of the routine note."); fragment.append(sampleEl); const updateSample = () => { - sampleEl.textContent = `Sample: ${momentProvider.getNow()}`; + sampleEl.textContent = `Sample: ${Day.fromNow().getAsUserCustomFormat()}`; }; updateSample(); // 초기화 return { diff --git a/src/settings/suggesters/FileSuggester.ts b/src/settings/suggesters/FileSuggester.ts index b478d04..b77ccd0 100644 --- a/src/settings/suggesters/FileSuggester.ts +++ b/src/settings/suggesters/FileSuggester.ts @@ -2,7 +2,7 @@ import { TAbstractFile, TFile, TFolder } from "obsidian"; import { TextInputSuggest } from "./suggest"; -import { plugin } from "src/shared/utils/plugin-service-locator"; +import { plugin } from "lib/plugin-service-locator"; export class FileSuggest extends TextInputSuggest { diff --git a/src/settings/suggesters/suggest.ts b/src/settings/suggesters/suggest.ts index 87f61b3..5a8455a 100644 --- a/src/settings/suggesters/suggest.ts +++ b/src/settings/suggesters/suggest.ts @@ -2,7 +2,7 @@ import { ISuggestOwner, Scope } from "obsidian"; import { createPopper, Instance as PopperInstance } from "@popperjs/core"; -import { plugin } from "src/shared/utils/plugin-service-locator"; +import { plugin } from "lib/plugin-service-locator"; const wrapAround = (value: number, size: number): number => { return ((value % size) + size) % size; diff --git a/src/shared/utils/moment-provider.ts b/src/shared/utils/moment-provider.ts deleted file mode 100644 index 5bf64a3..0000000 --- a/src/shared/utils/moment-provider.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { moment } from "obsidian"; -import { plugin } from "./plugin-service-locator"; -import { DEFAULT_SETTINGS } from "src/settings/DailyRoutineSettingTab"; - - -const gerFormat = (): string => { - const format = plugin().settings.dateFormat; - if(format && format !== '') { - return format; - } else { - return DEFAULT_SETTINGS.dateFormat as string; - } -} - -export const momentProvider = { - - getNow: () => { - return moment().format(gerFormat()); - }, - - // 0, 1, 2, 3, 4, 5, 6 - getDayOfWeekNum: () => { - return Number(moment().format('d')); - }, - - // "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - getDayOfWeek: () => { - return moment().format('ddd'); - }, - - isToday: (dataTime: string) => { - return moment(momentProvider.getNow()).isSame(dataTime); - } -} -export enum DayOfWeek { - SUN, - MON, - TUE, - WEN, - THU, - FRI, - SAT -} diff --git a/src/view/daily-routine-view.tsx b/src/view/daily-routine-view.tsx index 9260d32..8070e2f 100644 --- a/src/view/daily-routine-view.tsx +++ b/src/view/daily-routine-view.tsx @@ -1,8 +1,9 @@ import { WorkspaceLeaf } from "obsidian"; -import { ReactView } from "../shared/view/react-view"; -import { RoutineNoteView } from "./routine-note-view"; +import { ReactView } from "../lib/view/react-view"; +import { RoutineNoteView } from "./routine-note"; import { createNewRoutineNote, RoutineNote } from "../model/routine-note"; import { useEffect, useState } from "react"; +import { Day } from "lib/day"; export class DailyRoutineView extends ReactView { @@ -27,6 +28,7 @@ export class DailyRoutineView extends ReactView { const DailyRoutineViewComponent = () => { const [routineNote, setRoutineNote] = useState({ routines: [], + day: Day.fromNow(), title: "Fallback" }); diff --git a/src/view/routine-note-view.tsx b/src/view/routine-note-view.tsx deleted file mode 100644 index c7a13b5..0000000 --- a/src/view/routine-note-view.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { RoutineNote } from "../model/routine-note"; -import { RoutineComponent } from "./routine"; - - - -interface Props { - routineNote: RoutineNote -} -export const RoutineNoteView = ({ routineNote }: Props) => { - return ( -
- {routineNote.routines.map((routine, idx) => { - return ( -
- -
- ) - })} -
- ); -} diff --git a/src/view/routine-note/index.css b/src/view/routine-note/index.css new file mode 100644 index 0000000..56cf986 --- /dev/null +++ b/src/view/routine-note/index.css @@ -0,0 +1,14 @@ + + +.dr-note__header { + display: flex; + justify-content: start; + align-items: end; + margin: 1em; + gap: 10px; +} + +.dr-note__header h1 { + display: inline-block; + margin: 0; +} \ No newline at end of file diff --git a/src/view/routine-note/index.tsx b/src/view/routine-note/index.tsx new file mode 100644 index 0000000..2466c8a --- /dev/null +++ b/src/view/routine-note/index.tsx @@ -0,0 +1,38 @@ +import { RoutineNote } from "../../model/routine-note"; +import { RoutineComponent } from "../routine"; +import React, { useCallback } from "react"; +import "./index.css"; +import { Routine, routineManager } from "model/routine"; + + + +interface Props { + routineNote: RoutineNote +} +export const RoutineNoteView = ({ routineNote }: Props) => { + + const onCheckChange = useCallback((routine: Routine, checked: boolean) => { + routineManager.updateAchievement({ + routineName: routine.name, + day: routineNote.day, + checked + }); + }, [routineNote]); + + + return ( +
+
+

Daily Routine

+ {routineNote.title} +
+ {routineNote.routines.map((routine, idx) => { + return ( +
+ +
+ ) + })} +
+ ); +} diff --git a/src/view/routine/index.css b/src/view/routine/index.css new file mode 100644 index 0000000..e408254 --- /dev/null +++ b/src/view/routine/index.css @@ -0,0 +1,141 @@ + + +/* ##################################################################### + * ####################### GLOBAL STYLES ############################### + * ##################################################################### */ +.app-container { + --color-accent-1-hsl: calc(var(--accent-h) - 3), calc(var(--accent-s) * 1.02), calc(var(--accent-l) * 1.15); + --color-accent-2-hsl: calc(var(--accent-h) - 5), calc(var(--accent-s) * 1.05), calc(var(--accent-l) * 1.29); +} + +/* ##################################################################### + * ####################### ROUTINE COMPONENT ########################### + * ##################################################################### */ + +/* 최상위 루틴 */ +.dr-routine { + display: block; + font-size: 16px; /* 전체적 루틴의 사이즈를 결정 */ + width: 100%; + padding: calc(0.5em) 0; + /* border: 1px solid #c8ccd4; */ + cursor: pointer; +} + + + +/* 전체 감싸주는 라벨 */ +.dr-routine label { + margin: 0; + padding: 0; + border: 0; + font: inherit; + vertical-align: baseline; + line-height: 1; +} + +/* 레이아웃 */ +.dr-routine *, +.dr-routine ::after, +.dr-routine ::before { + box-sizing: border-box; +} + +/* 보이지 않는 데이터용 input checkbox */ +.dr-routine input[type="checkbox"] { + visibility: hidden; + display: none; +} + +/* 실제로 보이는 디자인용 체크박스 */ +.dr-routine .dr-routine__item .dr-routine__cbx { + position: relative; + top: calc(1/8 * 1em); + display: inline-block; + width: calc(14/16 * 1em); + height: calc(14/16 * 1em); + margin-right: calc(0.5em); + border: calc(1/16 * 1em) solid #c8ccd4; + border-radius: 3px; /* 체크박스 곡률 */ + cursor: pointer; +} +.dr-routine .dr-routine__item .dr-routine__cbx svg { + position: relative; + width: calc(7/8 * 1em); + height: calc(3/4 * 1em); + top: calc(-1/16 * 1em); + transform: scale(0); + fill: none; + stroke-linecap: round; + stroke-linejoin: round; +} +.dr-routine .dr-routine__item .dr-routine__cbx svg polyline { + stroke-width: 2.2; + stroke: hsla(var(--color-accent-1-hsl), 1.0); +} +/* 체크 표시 아이콘 */ +.dr-routine .dr-routine__item .dr-routine__cbx:before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + margin: calc(-5/8 * 1em) 0 0 calc(-5/8 * 1em); + width: calc(5/4 * 1em); + height: calc(5/4 * 1em); + border-radius: 100%; + background: hsla(var(--color-accent-1-hsl), 1.0); + transform: scale(0); +} +/* 체크 이펙트 */ +.dr-routine .dr-routine__item .dr-routine__cbx:after { + content: ''; + position: absolute; + top: calc(5/16 * 1em); + left: calc(5/16 * 1em); + width: calc(1/8 * 1em); + height: calc(1/8 * 1em); + border-radius: 2px; + box-shadow: 0 calc(9/8 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * 1em) calc(3/4 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(9/8 * 1em) 0 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * 1em) calc(3/4 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), 0 calc(9/8 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * -1em) calc(3/4 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(9/8 * -1em) 0 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * -1em) calc(3/4 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0); + transform: scale(0); +} +.dr-routine .dr-routine__item input:checked + .dr-routine__cbx { + border-color: transparent; +} +.dr-routine .dr-routine__item input:checked + .dr-routine__cbx svg { + transform: scale(1); + transition: all 0.4s ease; + transition-delay: 0.1s; +} +.dr-routine .dr-routine__item input:checked + .dr-routine__cbx:before { + transform: scale(1); + opacity: 0; + transition: all 0.3s ease; +} +.dr-routine .dr-routine__item input:checked + .dr-routine__cbx:after { + transform: scale(1); + opacity: 0; + transition: all 0.6s ease; +} + +/* 루틴 이름 */ +.dr-routine .dr-routine__item .dr-routine__name { + position: relative; + cursor: pointer; + transition: color 0.3s ease; +} +.dr-routine .dr-routine__item .dr-routine__name:after { + content: ''; + position: absolute; + top: 50%; + left: 0; + width: 0; + height: 1px; + background: #9098a9; +} +.dr-routine .dr-routine__item input:checked ~ .dr-routine__name { + color: #9098a9; +} +.dr-routine .dr-routine__item input:checked ~ .dr-routine__name:after { + width: 100%; + transition: all 0.4s ease; +} \ No newline at end of file diff --git a/src/view/routine.tsx b/src/view/routine/index.tsx similarity index 62% rename from src/view/routine.tsx rename to src/view/routine/index.tsx index a6a2f3a..24cd125 100644 --- a/src/view/routine.tsx +++ b/src/view/routine/index.tsx @@ -1,11 +1,15 @@ -import { Routine } from "../model/routine" +import { Routine } from "../../model/routine" +import React from "react" +import "./index.css" interface Props { - routine: Routine + routine: Routine; + onCheckChange: (routine: Routine, checked: boolean) => void; } -export const RoutineComponent = ({ routine }: Props) => { +export const RoutineComponent = ({ routine, onCheckChange }: Props) => { + const id = `routine-${routine.name}` return ( @@ -13,7 +17,7 @@ export const RoutineComponent = ({ routine }: Props) => { {/* 전체적으로 감싸주는 label */}