mirror of
https://github.com/sechan100/daily-routine-2.git
synced 2026-07-22 05:37:51 +00:00
루틴 체크시 루틴파일에 achievement 기록하는 기능 개발
This commit is contained in:
parent
3c1f60c8a1
commit
a0944057b1
26 changed files with 494 additions and 171 deletions
|
|
@ -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();
|
||||
}
|
||||
await context.watch();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const devOnlyTest = () => {
|
|||
|
||||
|
||||
const test = () => {
|
||||
routineManager.editRoutine('🖊️ 문서 작성하기', {
|
||||
routineManager.edit('🖊️ 문서 작성하기', {
|
||||
name: '🖊️ 문서 작성하기ㅋ',
|
||||
newProperties: {
|
||||
dayOfWeeks: [1, 2, 3, 4, 5, 6, 0]
|
||||
|
|
|
|||
1
src/index.d.ts
vendored
Normal file
1
src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
declare module '*.css';
|
||||
84
src/lib/day.ts
Normal file
84
src/lib/day.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Editor, MarkdownView } from "obsidian";
|
||||
import { plugin } from "./plugin-service-locator"
|
||||
import { plugin } from "../plugin-service-locator"
|
||||
|
||||
|
||||
|
||||
|
|
@ -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에 해당하는 뷰를 활성화한다.
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<RoutineNote> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RoutineProperties>
|
||||
}) => {
|
||||
|
|
@ -68,14 +69,84 @@ export const routineManager = {
|
|||
* 루틴 가져오기
|
||||
* @param routineName 루틴파일명: identifier
|
||||
*/
|
||||
getRoutine: async (routineName: string): Promise<Routine> => {
|
||||
get: async (routineName: string): Promise<Routine> => {
|
||||
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}`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 루틴 이름으로부터 루틴 파일의 경로를 가져온다.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<TAbstractFile> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<RoutineNote>({
|
||||
routines: [],
|
||||
day: Day.fromNow(),
|
||||
title: "Fallback"
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { RoutineNote } from "../model/routine-note";
|
||||
import { RoutineComponent } from "./routine";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
routineNote: RoutineNote
|
||||
}
|
||||
export const RoutineNoteView = ({ routineNote }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
{routineNote.routines.map((routine, idx) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<RoutineComponent routine={routine} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/view/routine-note/index.css
Normal file
14
src/view/routine-note/index.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
38
src/view/routine-note/index.tsx
Normal file
38
src/view/routine-note/index.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="dr-note">
|
||||
<header className="dr-note__header">
|
||||
<h1>Daily Routine</h1>
|
||||
<span>{routineNote.title}</span>
|
||||
</header>
|
||||
{routineNote.routines.map((routine, idx) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<RoutineComponent onCheckChange={onCheckChange} routine={routine} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
src/view/routine/index.css
Normal file
141
src/view/routine/index.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
<label htmlFor={id} className="dr-routine__item">
|
||||
{/* 체크박스(hidden) */}
|
||||
<input type="checkbox" id={id} className="hidden"/>
|
||||
<input onChange={(e) => onCheckChange(routine, e.target.checked)} type="checkbox" id={id} className="hidden"/>
|
||||
{/* 체크박스(display) */}
|
||||
<label htmlFor={id} className="dr-routine__cbx">
|
||||
<svg viewBox="0 0 14 12">
|
||||
79
styles.css
79
styles.css
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"baseUrl": "src/",
|
||||
"jsx": "react-jsx",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
|
|
@ -19,7 +21,5 @@
|
|||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
, "src/model/react-view.tsx" ]
|
||||
"include": ["**/*.ts", "**/*.tsx", "src/index.d.ts"],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue