mirror of
https://github.com/sechan100/daily-routine-2.git
synced 2026-07-22 05:37:51 +00:00
refactor: 전체 뷰 레이아웃 개편
- MUI를 사용하여 tabs를 구현 - 레이아웃이 깨지거나 침범하는 부분들을 깔끔하게 정리
This commit is contained in:
parent
ed79579018
commit
2fefe2ed9e
22 changed files with 615 additions and 334 deletions
|
|
@ -107,7 +107,8 @@ async function copyPluginFilesTo_iCloud() {
|
|||
];
|
||||
|
||||
const destinationDir = '/Users/sechan/Library/Mobile Documents/iCloud~md~obsidian/Documents/main/.obsidian/plugins/daily-routine-2/';
|
||||
// const destinationDir = '/Users/sechan/local/obsidian/plugin-dev';
|
||||
// const destinationDir = '/Users/sechan/Library/Mobile Documents/iCloud~md~obsidian/Documents/cloud-test/.obsidian/plugins/daily-routine-2/';
|
||||
|
||||
|
||||
// 현재 파일의 URL을 파일 경로로 변환
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
|
|
|||
250
package-lock.json
generated
250
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/material": "^6.1.6",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.1.1",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
|
|
@ -194,9 +195,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.25.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
|
||||
"integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
|
|
@ -935,6 +936,213 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz",
|
||||
"integrity": "sha512-nz1SlR9TdBYYPz4qKoNasMPRiGb4PaIHFkzLzhju0YVYS5QSuFF2+n7CsiHMIDcHv3piPu/xDWI53ruhOqvZwQ==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.6.tgz",
|
||||
"integrity": "sha512-1yvejiQ/601l5AK3uIdUlAVElyCxoqKnl7QA+2oFB/2qYPWfRwDgavW/MoywS5Y2gZEslcJKhe0s2F3IthgFgw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/core-downloads-tracker": "^6.1.6",
|
||||
"@mui/system": "^6.1.6",
|
||||
"@mui/types": "^7.2.19",
|
||||
"@mui/utils": "^6.1.6",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.11",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^18.3.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^6.1.6",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"@mui/material-pigment-css": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.6.tgz",
|
||||
"integrity": "sha512-ioAiFckaD/fJSnTrUMWgjl9HYBWt7ixCh7zZw7gDZ+Tae7NuprNV6QJK95EidDT7K0GetR2rU3kAeIR61Myttw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/utils": "^6.1.6",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.6.tgz",
|
||||
"integrity": "sha512-I+yS1cSuSvHnZDBO7e7VHxTWpj+R7XlSZvTC4lS/OIbUNJOMMSd3UDP6V2sfwzAdmdDNBi7NGCRv2SZ6O9hGDA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@emotion/cache": "^11.13.1",
|
||||
"@emotion/serialize": "^1.3.2",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.6.tgz",
|
||||
"integrity": "sha512-qOf1VUE9wK8syiB0BBCp82oNBAVPYdj4Trh+G1s+L+ImYiKlubWhhqlnvWt3xqMevR+D2h1CXzA1vhX2FvA+VQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/private-theming": "^6.1.6",
|
||||
"@mui/styled-engine": "^6.1.6",
|
||||
"@mui/types": "^7.2.19",
|
||||
"@mui/utils": "^6.1.6",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.19",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz",
|
||||
"integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.6.tgz",
|
||||
"integrity": "sha512-sBS6D9mJECtELASLM+18WUcXF6RH3zNxBRFeyCRg8wad6NbyNrdxLuwK+Ikvc38sTZwBzAz691HmSofLqHd9sQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/types": "^7.2.19",
|
||||
"@types/prop-types": "^15.7.13",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^18.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -1337,14 +1545,12 @@
|
|||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
|
||||
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
|
|
@ -1359,6 +1565,14 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
|
||||
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
|
|
@ -2424,6 +2638,15 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
|
||||
|
|
@ -7515,6 +7738,21 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/material": "^6.1.6",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.1.1",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
/** @jsxImportSource @emotion/react */
|
||||
import { css } from "@emotion/react";
|
||||
import { RoutineNote } from 'pages/routine-note';
|
||||
import { RoutineCalendar } from "pages/calendar";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Day } from "shared/day";
|
||||
import { usePageRoute } from "./use-page-route";
|
||||
import "./style.css";
|
||||
import { DailyRoutineObsidianView } from './obsidian-view';
|
||||
import { useLeaf } from 'shared/view/react-view';
|
||||
import { Button } from 'shared/components/Button';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import { MUIThemeProvider } from './MUIThemProvider';
|
||||
import { Box } from '@mui/material';
|
||||
import { Icon } from "shared/components/Icon";
|
||||
|
||||
|
||||
|
||||
const pageNavbarHeight = 50;
|
||||
const tabsHeight = "50px";
|
||||
const tabsBottomGap = "25px";
|
||||
|
||||
const tabCss = css({
|
||||
boxShadow: "none !important",
|
||||
backgroundColor: "transparent !important",
|
||||
minHeight: "0 !important",
|
||||
fontSize: "0.7em",
|
||||
})
|
||||
|
||||
|
||||
// FIXME: setPage로 렌더링 1번, cmd를 적용해서 다시 렌더링까지 총 2번의 렌더링이 발생한다. 추후 여건되면 최적화 가능
|
||||
|
|
@ -24,6 +37,9 @@ export const DailyRoutineView = () => {
|
|||
}, [routePage]);
|
||||
|
||||
const { view } = useLeaf<DailyRoutineObsidianView>();
|
||||
useEffect(() => {
|
||||
view.contentEl.style.padding = "0";
|
||||
}, [view]);
|
||||
|
||||
const viewContentRealHeight = useMemo(() => {
|
||||
const viewContainer = view.contentEl;
|
||||
|
|
@ -36,56 +52,35 @@ export const DailyRoutineView = () => {
|
|||
|
||||
|
||||
return (
|
||||
<div
|
||||
className='dr-page'
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
height: (viewContentRealHeight - pageNavbarHeight - 1), // 1은 offset
|
||||
}}
|
||||
>
|
||||
{page === "note" && <RoutineNote day={routedDay} />}
|
||||
{page === "calendar" && <RoutineCalendar onDayTileClick={onDayTileClick} defaultDay={routedDay} />}
|
||||
<div
|
||||
<MUIThemeProvider>
|
||||
<div
|
||||
className="dr-page"
|
||||
css={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "stretch",
|
||||
position: "fixed",
|
||||
width: "100%",
|
||||
height: pageNavbarHeight,
|
||||
left: "0",
|
||||
bottom: "0",
|
||||
height: `calc(100% - ${tabsHeight} - ${tabsBottomGap})`,
|
||||
}}
|
||||
>
|
||||
<ViewNavItem active={page==="note"} onClick={() => routePage("note", Day.now())}>Routine Note</ViewNavItem>
|
||||
<ViewNavItem active={page==="calendar"} onClick={() => routePage("calendar", Day.now())}>Calendar</ViewNavItem>
|
||||
<ViewNavItem active={page==="achivement"} onClick={() => routePage("achivement", Day.now())}>Achivement</ViewNavItem>
|
||||
{page === "note" && <RoutineNote day={routedDay} />}
|
||||
{page === "calendar" && <RoutineCalendar onDayTileClick={onDayTileClick} defaultDay={routedDay} />}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
value={page}
|
||||
className="dr-tabs"
|
||||
scrollButtons={false}
|
||||
centered
|
||||
sx={{ borderTop: 1, borderColor: 'divider' }}
|
||||
onChange={(e, value) => routePage(value, Day.now())}
|
||||
css={{
|
||||
position: "fixed",
|
||||
height: tabsHeight,
|
||||
width: "100%",
|
||||
left: "0",
|
||||
bottom: tabsBottomGap,
|
||||
}}
|
||||
>
|
||||
<Tab icon={<Icon icon="notebook-pen" />} iconPosition="start" label="Note" css={tabCss} value={"note"} />
|
||||
<Tab icon={<Icon icon="calendar-check-2" />} iconPosition="start" label="Calendar" css={tabCss} value={"calendar"} />
|
||||
<Tab icon={<Icon icon="book-check" />} iconPosition="start" label="Achivement" css={tabCss} value={"achivement"} />
|
||||
</Tabs>
|
||||
</MUIThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface ViewNavItemProps {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const ViewNavItem = ({ active, onClick, children }: ViewNavItemProps) => {
|
||||
return (
|
||||
<Button
|
||||
css={{
|
||||
display: "block",
|
||||
alignSelf: "stretch",
|
||||
height: "auto",
|
||||
width: "100%",
|
||||
borderRadius: "0.2em 0.2em 0 0",
|
||||
fontWeight: "var(--font-weight)"
|
||||
}}
|
||||
accent={active}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
54
src/app/MUIThemProvider.tsx
Normal file
54
src/app/MUIThemProvider.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Theme, ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import _ from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
interface MUIThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export const MUIThemeProvider = (props: MUIThemeProviderProps) => {
|
||||
const body = useMemo(() => {
|
||||
const body = document.querySelector("body");
|
||||
if(!body) throw new Error("Cannot find body element");
|
||||
return body;
|
||||
}, []);
|
||||
|
||||
const extractMUIThemeFromBodyEl = useCallback(() => {
|
||||
const computedStyle = getComputedStyle(body);
|
||||
return createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: computedStyle.getPropertyValue('--color-accent-1').trim(),
|
||||
},
|
||||
// secondary: {},
|
||||
},
|
||||
});
|
||||
}, [body]);
|
||||
|
||||
const [ theme, setTheme ] = useState<Theme>(extractMUIThemeFromBodyEl());
|
||||
|
||||
useEffect(() => {
|
||||
const updateThemeColor = _.debounce(() => {
|
||||
setTheme(extractMUIThemeFromBodyEl())
|
||||
}, 100);
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
||||
updateThemeColor();
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(body, { attributes: true, attributeFilter: ['style'] });
|
||||
}, [body, extractMUIThemeFromBodyEl, theme]);
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
{props.children}
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import { DailyRoutineView } from "./DailyRoutineView";
|
|||
|
||||
|
||||
|
||||
|
||||
export class DailyRoutineObsidianView extends ReactView {
|
||||
static VIEW_TYPE = "daily-routine-view";
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { DaysNav } from "./DaysNav";
|
||||
|
|
@ -1,98 +1,26 @@
|
|||
/** @jsxImportSource @emotion/react */
|
||||
import { Day } from "shared/day";
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import "./days-nav.scss";
|
||||
import { PercentageCircle } from "shared/components/PercentageCircle";
|
||||
import { Navigation } from 'swiper/modules';
|
||||
import { Swiper, SwiperClass, SwiperRef, SwiperSlide } from 'swiper/react';
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import { routineNoteArchiver, routineNoteService, useRoutineNote } from 'entities/note';
|
||||
import { useRoutineNote } from 'entities/note';
|
||||
import { dr } from "shared/daily-routine-bem";
|
||||
import { css } from "@emotion/react";
|
||||
import { loadWeeks } from "./load-weeks";
|
||||
|
||||
|
||||
|
||||
// week를 로드하는 함수. option은 앞뒤로 추가할 week의 수. prev: -1, next: 2라면 총 4개의 week를 로드한다.
|
||||
const loadWeek = async (day: Day, {prev, next}: {prev: number, next: number} = {prev: 0, next: 0}) => {
|
||||
const start = day.subtractOnClone(prev*7, "day");
|
||||
const end = day.addOnClone((next + 1)*7, "day").subtract(1, "day"); // week의 시작날짜는 포함이기에 마지막에 하나 빼줘야함.
|
||||
|
||||
// 존재하는 루틴 노트들 불러오기
|
||||
const notes = (await routineNoteArchiver.loadBetween(start, end)).map(routineNote => {
|
||||
const day = routineNote.day;
|
||||
const percentage = routineNoteService.getTaskCompletion(routineNote).percentageRounded;
|
||||
return {day, percentage};
|
||||
});
|
||||
|
||||
/**
|
||||
* notes은 이미 존재하는 note들만 가져오기때문에, 원래 요청된 사이즈만큼의 노트가 존재하지 않을 수 있음.
|
||||
* 때문에 빈노트 element들을 만들어서 채워주어야하는데, 이 때 사용될 수 있는 요청된 사이즈만큼의 배열이 채워졌는지 확인하는 함수.
|
||||
*/
|
||||
const isRequestedSize = () => notes.length === (prev+next+1)*7;
|
||||
|
||||
/**
|
||||
* notes의 시작과 끝을 먼저 정해진 경계값으로 채우고, 사이를 매꾸는 방식을 사용한다.
|
||||
* 다만, notes중에서 비어있는 노트들이 이 경계값인 경우가 있을 수 있다.
|
||||
* 때문에 이를 확인해서 경계값이 비어있는 경우 먼저 채워준다.
|
||||
*/
|
||||
if(notes.length === 0){ // 아예 가져온 노트가 없다면 경계가 모두 없음
|
||||
notes.push({day: start.clone(), percentage: 0});
|
||||
notes.push({day: end.clone(), percentage: 0});
|
||||
} else {
|
||||
if(!notes[0].day.isSameDay(start)){
|
||||
notes.unshift({day: start.clone(), percentage: 0});
|
||||
}
|
||||
if(!notes[notes.length-1].day.isSameDay(end)){
|
||||
notes.push({day: end.clone(), percentage: 0});
|
||||
}
|
||||
}
|
||||
|
||||
// 매꾸기
|
||||
for(let i = 0; i+1 < notes.length; i++){
|
||||
const note = notes[i];
|
||||
const nextNote = notes[i+1];
|
||||
const isContinueous = () => note.day.addOnClone(1, "day").isSameDay(nextNote.day);
|
||||
if(!isContinueous()){
|
||||
const emptyNote = {day: note.day.addOnClone(1, "day"), percentage: 0};
|
||||
notes.splice(i+1, 0, emptyNote);
|
||||
}
|
||||
if(isRequestedSize()) break;
|
||||
}
|
||||
|
||||
// 하나의 배열에 담겨있는 day들을 week의 2차원 배열로 변환
|
||||
const weeks: {day:Day, percentage:number}[][] = new Array(prev+next+1).fill([]).map(() => []);
|
||||
let weekIdx = 0;
|
||||
for(const currentDay of notes){
|
||||
// 최초 week에 추가
|
||||
const currentWeek = weeks[weekIdx];
|
||||
if(currentWeek.length === 0){
|
||||
currentWeek.push(currentDay);
|
||||
continue;
|
||||
}
|
||||
// 만약 week가 꽉 찼다면 weekIdx를 증가시키고 다음 week에 추가
|
||||
if(weeks[weekIdx].length === 7){
|
||||
weekIdx++;
|
||||
if(weekIdx >= weeks.length) break;
|
||||
weeks[weekIdx].push(currentDay);
|
||||
continue;
|
||||
}
|
||||
weeks[weekIdx].push(currentDay);
|
||||
}
|
||||
return weeks;
|
||||
};
|
||||
|
||||
|
||||
interface DaysNavProps {
|
||||
interface WeeksProps {
|
||||
currentDay: Day;
|
||||
/**
|
||||
* 현재 보고있는 day의 percentage는, note page에서 사용자가 task를 클릭할 때마다 동적으로 변경될 수 있다.
|
||||
*/
|
||||
currentDayPercentage: number;
|
||||
resizeObserver?: (entry: ResizeObserverEntry) => void;
|
||||
onDayClick?: (day: Day, event?: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
export const DaysNav = ({ currentDay, resizeObserver, currentDayPercentage, onDayClick }: DaysNavProps) => {
|
||||
export const Weeks = ({ currentDay, currentDayPercentage, onDayClick, className }: WeeksProps) => {
|
||||
const [ weeks, setWeeks ] = useState<{day:Day, percentage:number}[][]>([]);
|
||||
const currentWeekStartDay = useMemo(() => currentDay.clone(m => m.startOf("week")), [currentDay]);
|
||||
const swiperRef = useRef<SwiperRef>(null);
|
||||
|
|
@ -136,7 +64,7 @@ export const DaysNav = ({ currentDay, resizeObserver, currentDayPercentage, onDa
|
|||
if(ignore) return;
|
||||
// weeks에 currentWeekStartDay가 포함되어있지 않다면, 해당 week를 로드한다.
|
||||
if(!weeks.some(week => week.some(({day}) => day.isSameDay(currentWeekStartDay)))){
|
||||
loadWeek(currentWeekStartDay, {prev: 1, next: 1}) // 해당 week와 앞뒤로 하나씩의 week를 로드한다.
|
||||
loadWeeks(currentWeekStartDay, {prev: 1, next: 1}) // 해당 week와 앞뒤로 하나씩의 week를 로드한다.
|
||||
.then(weeks => {
|
||||
setWeeks(weeks);
|
||||
// 새롭게 3개의 week를 로드했으므로, swiper를 중간으로 이동시킨다.
|
||||
|
|
@ -168,7 +96,7 @@ export const DaysNav = ({ currentDay, resizeObserver, currentDayPercentage, onDa
|
|||
*/
|
||||
const onSlideToEdge = useCallback(async(edge: "start" | "end") => {
|
||||
const needWeekStartDay = edge === "start" ? weeks[0][0].day.subtractOnClone(1, "week") : weeks[weeks.length-1][0].day.addOnClone(1, "week");
|
||||
const newWeek = (await loadWeek(needWeekStartDay))[0]; // 새로운 week 하나만 받아옴으로 0번으로 가져옴
|
||||
const newWeek = (await loadWeeks(needWeekStartDay))[0]; // 새로운 week 하나만 받아옴으로 0번으로 가져옴
|
||||
if(edge === "start"){
|
||||
updateWeeksRef.current = () => {
|
||||
setWeeks([newWeek, ...weeks]);
|
||||
|
|
@ -201,40 +129,37 @@ export const DaysNav = ({ currentDay, resizeObserver, currentDayPercentage, onDa
|
|||
if(onDayClick) onDayClick(day, event);
|
||||
}, [onDayClick, setClientNote]);
|
||||
|
||||
const bem = useMemo(() => dr("days-nav"), []);
|
||||
const bem = useMemo(() => dr("weeks"), []);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
&.hidden-navigation {
|
||||
& .swiper-button-next, & .swiper-button-prev {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
<div
|
||||
className={bem("", "", className)}
|
||||
onTouchStart={(e) => e.stopPropagation()}
|
||||
css={{
|
||||
padding: "0 5px",
|
||||
// nav buttons
|
||||
& .swiper-button-next, & .swiper-button-prev {
|
||||
color: var(--color-accent-1);
|
||||
|
||||
"& .swiper-button-next, & .swiper-button-prev": {
|
||||
display: "flex",
|
||||
color: "var(--color-accent-1)",
|
||||
// 화살표 사이즈
|
||||
&::after {
|
||||
font-size: 2em;
|
||||
"&::after": {
|
||||
fontSize: "1em",
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
`}
|
||||
className={bem()}
|
||||
ref={ref => {
|
||||
if(!ref) return;
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.contentRect.width < 540) {
|
||||
ref.classList.add("hidden-navigation");
|
||||
} else {
|
||||
ref.classList.remove("hidden-navigation");
|
||||
}
|
||||
resizeObserver?.(entry);
|
||||
},
|
||||
".swiper-button-next": {
|
||||
right: "0px",
|
||||
justifyContent: "end",
|
||||
},
|
||||
".swiper-button-prev": {
|
||||
left: "0px",
|
||||
justifyContent: "start",
|
||||
},
|
||||
".is-mobile &": {
|
||||
".swiper-button-next, .swiper-button-prev": {
|
||||
display: "none"
|
||||
}
|
||||
});
|
||||
observer.observe(ref);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Swiper
|
||||
|
|
@ -255,61 +180,86 @@ export const DaysNav = ({ currentDay, resizeObserver, currentDayPercentage, onDa
|
|||
>
|
||||
{weeks.map((week, idx) => {
|
||||
return (
|
||||
<SwiperSlide
|
||||
key={idx}
|
||||
className={clsx(bem("slide"), bem("week"))}
|
||||
>
|
||||
<SwiperSlide key={idx} className={bem("slide")}>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
min-width: 290px;
|
||||
max-width: 450px;
|
||||
margin: 0 auto;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
flex-wrap: nowrap;
|
||||
className={bem("week")}
|
||||
css={{
|
||||
display: "flex",
|
||||
height: "5em",
|
||||
maxWidth: "470px",
|
||||
minWidth: "300px",
|
||||
margin: "0 auto",
|
||||
justifyContent: "center",
|
||||
alignItems: "stretch",
|
||||
// gap: "0.3em",
|
||||
flexWrap: "nowrap",
|
||||
|
||||
// 겹쳤을 때, bg가 투명해서 겹쳐보이는 것을 방지
|
||||
.workspace-tabs .workspace-leaf & {
|
||||
background-color: var(--background-secondary);
|
||||
".workspace-tabs .workspace-leaf &": {
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
},
|
||||
".workspace-split.mod-root .view-content &": {
|
||||
backgroundColor: "var(--background-primary)",
|
||||
}
|
||||
.workspace-split.mod-root .view-content & {
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
`}
|
||||
}}
|
||||
>
|
||||
{week.map(({day, percentage}, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
key={idx}
|
||||
className={bem("day", {
|
||||
"today": day.isSameDay(Day.now()),
|
||||
"after": day.isAfter(Day.now()),
|
||||
"current": day.isSameDay(currentDay)
|
||||
})}
|
||||
css={css`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: auto;
|
||||
background: none;
|
||||
padding: 0 0em;
|
||||
cursor: pointer;
|
||||
border-radius: 7px;
|
||||
${day.isSameDay(currentDay) && css`
|
||||
box-shadow: inset 0 0 0.5em 0.1em rgba(0, 0, 0, 0.1) !important;
|
||||
`}
|
||||
`}
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
gap: "0.3em",
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
background: "none",
|
||||
padding: "0 1.5px",
|
||||
cursor: "pointer",
|
||||
borderRadius: "7px",
|
||||
|
||||
"&.dr-weeks__day--after::after": {
|
||||
content: "''",
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
bottom: "0",
|
||||
borderRadius: "7px",
|
||||
background: "var(--background-primary)",
|
||||
opacity: "0.7",
|
||||
},
|
||||
"&.dr-weeks__day--current": {
|
||||
boxShadow: "inset 0 0 2px 0.5px rgba(0, 0, 0, 0.5) !important",
|
||||
"&::after": {
|
||||
boxShadow: "inset 0 0 2px 0.5px rgba(0, 0, 0, 0.5) !important",
|
||||
// content: "none",
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClick={(e) => _onDayClick(day, e)}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
`}
|
||||
css={{
|
||||
textAlign: "center",
|
||||
fontSize: "0.8em",
|
||||
height: "1em",
|
||||
}}
|
||||
>
|
||||
{day.format("M/D").toUpperCase()}
|
||||
</div>
|
||||
<PercentageCircle
|
||||
css={{
|
||||
position: "relative",
|
||||
bottom: "0px",
|
||||
}}
|
||||
width="100%"
|
||||
percentage={percentage}
|
||||
transition={circleTransitionRef.current}
|
||||
text={day.format("ddd").toUpperCase()}
|
||||
1
src/features/weeks/index.ts
Normal file
1
src/features/weeks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Weeks } from "./Weeks";
|
||||
82
src/features/weeks/load-weeks.tsx
Normal file
82
src/features/weeks/load-weeks.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { routineNoteArchiver, routineNoteService } from "entities/note";
|
||||
import { Notice } from "obsidian";
|
||||
import { Day } from "shared/day";
|
||||
import { WeeksDayNode } from "./types";
|
||||
|
||||
|
||||
|
||||
type LoadWeeks = (day: Day, option?: { prev: number; next: number; }) => Promise<{ day: Day; percentage: number; }[][]>;
|
||||
|
||||
|
||||
// week를 로드하는 함수. option은 앞뒤로 추가할 week의 수. prev: -1, next: 2라면 총 4개의 week를 로드한다.
|
||||
export const loadWeeks: LoadWeeks = async (day, { prev, next } = { prev: 0, next: 0 }) => {
|
||||
const start = day.subtractOnClone(prev * 7, "day");
|
||||
const end = day.addOnClone((next + 1) * 7, "day").subtract(1, "day"); // week의 시작날짜는 포함이기에 마지막에 하나 빼줘야함.
|
||||
|
||||
const existNotes = await routineNoteArchiver.loadBetween(start, end);
|
||||
const nodes: WeeksDayNode[] = existNotes.map(routineNote => {
|
||||
const day = routineNote.day;
|
||||
const percentage = routineNoteService.getTaskCompletion(routineNote).percentageRounded;
|
||||
return { day, percentage };
|
||||
})
|
||||
|
||||
nodes.sort((a, b) => a.day.isBefore(b.day) ? -1 : 1);
|
||||
|
||||
|
||||
/**
|
||||
* notes은 이미 존재하는 note들만 가져오기때문에, 원래 요청된 사이즈만큼의 노트가 존재하지 않을 수 있음.
|
||||
* 때문에 빈노트 element들을 만들어서 채워주어야하는데, 이 때 사용될 수 있는 요청된 사이즈만큼의 배열이 채워졌는지 확인하는 함수.
|
||||
*/
|
||||
const isRequestedSize = () => nodes.length === (prev + next + 1) * 7;
|
||||
|
||||
/**
|
||||
* notes의 시작과 끝을 먼저 정해진 경계값으로 채우고, 사이를 매꾸는 방식을 사용한다.
|
||||
* 다만, notes중에서 비어있는 노트들이 이 경계값인 경우가 있을 수 있다.
|
||||
* 때문에 이를 확인해서 경계값이 비어있는 경우 먼저 채워준다.
|
||||
*/
|
||||
if (nodes.length === 0) { // 아예 가져온 노트가 없다면 경계가 모두 없음
|
||||
nodes.push({ day: start.clone(), percentage: 0 });
|
||||
nodes.push({ day: end.clone(), percentage: 0 });
|
||||
} else {
|
||||
if (!nodes[0].day.isSameDay(start)) {
|
||||
nodes.unshift({ day: start.clone(), percentage: 0 });
|
||||
}
|
||||
if (!nodes[nodes.length - 1].day.isSameDay(end)) {
|
||||
nodes.push({ day: end.clone(), percentage: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// 매꾸기
|
||||
for (let i = 0; i + 1 < nodes.length; i++) {
|
||||
const note = nodes[i];
|
||||
const nextNote = nodes[i + 1];
|
||||
const isContinueous = () => note.day.addOnClone(1, "day").isSameDay(nextNote.day);
|
||||
if (!isContinueous()) {
|
||||
const emptyNote = { day: note.day.addOnClone(1, "day"), percentage: 0 };
|
||||
nodes.splice(i + 1, 0, emptyNote);
|
||||
}
|
||||
if (isRequestedSize()) break;
|
||||
}
|
||||
|
||||
// 하나의 배열에 담겨있는 day들을 week의 2차원 배열로 변환
|
||||
const weeks: { day: Day; percentage: number; }[][] = new Array(prev + next + 1).fill([]).map(() => []);
|
||||
let weekIdx = 0;
|
||||
for (const currentDay of nodes) {
|
||||
// 최초 week에 추가
|
||||
const currentWeek = weeks[weekIdx];
|
||||
if (currentWeek.length === 0) {
|
||||
currentWeek.push(currentDay);
|
||||
continue;
|
||||
}
|
||||
// 만약 week가 꽉 찼다면 weekIdx를 증가시키고 다음 week에 추가
|
||||
if (weeks[weekIdx].length === 7) {
|
||||
weekIdx++;
|
||||
if (weekIdx >= weeks.length) break;
|
||||
weeks[weekIdx].push(currentDay);
|
||||
continue;
|
||||
}
|
||||
weeks[weekIdx].push(currentDay);
|
||||
}
|
||||
|
||||
return weeks;
|
||||
};
|
||||
7
src/features/weeks/types.ts
Normal file
7
src/features/weeks/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Day } from "shared/day";
|
||||
|
||||
|
||||
export type WeeksDayNode = {
|
||||
day: Day;
|
||||
percentage: number;
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/** @jsxImportSource @emotion/react */
|
||||
import { RoutineNote as RoutineNoteEntity, routineNoteService, routineNoteArchiver, UseRoutineNoteProvider, useRoutineNote } from 'entities/note';
|
||||
import { useStartRoutineModal } from "features/routine";
|
||||
import { DaysNav } from "features/days";
|
||||
import { Weeks } from "features/weeks";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Day } from "shared/day";
|
||||
import { dr } from "shared/daily-routine-bem";
|
||||
|
|
@ -54,8 +54,6 @@ const RoutineNotePage = () => {
|
|||
const { note, setNote } = useRoutineNote();
|
||||
const percentage = useMemo(() => routineNoteService.getTaskCompletion(note).percentageRounded, [note]);
|
||||
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const AddTodoModal = useAddTodoModal();
|
||||
const StartRoutineModal = useStartRoutineModal();
|
||||
|
||||
|
|
@ -84,76 +82,53 @@ const RoutineNotePage = () => {
|
|||
const bem = useMemo(() => dr("note"), []);
|
||||
return (
|
||||
<div
|
||||
className={bem()}
|
||||
css={{
|
||||
height: "100%",
|
||||
}}
|
||||
className={bem()}
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<DaysNav
|
||||
currentDay={note.day}
|
||||
currentDayPercentage={percentage}
|
||||
resizeObserver={entry => {
|
||||
const navHeight = entry.contentRect.height;
|
||||
if(mainRef.current){
|
||||
mainRef.current.style.height = `calc(100% - ${navHeight}px)`;
|
||||
}
|
||||
}}
|
||||
<Weeks
|
||||
className={bem("weeks")}
|
||||
currentDay={note.day}
|
||||
currentDayPercentage={percentage}
|
||||
/>
|
||||
<div className={bem("main")} ref={mainRef} css={{
|
||||
"--header-height": "4.5em",
|
||||
"--header-padding-y": "0.3em",
|
||||
}}>
|
||||
<header
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "var(--header-padding-y) 0.5em",
|
||||
height: "var(--header-height)",
|
||||
}}
|
||||
className={bem("header")}
|
||||
>
|
||||
<h4 css={{display: "inline-block"}}>
|
||||
{note.day.getBaseFormat() + " / " + note.day.getDayOfWeek()}
|
||||
</h4>
|
||||
<div css={{ display: "flex", gap: "1.5em", alignContent: "center"}}>
|
||||
<Button
|
||||
onClick={() => AddTodoModal.open({})}
|
||||
css={{
|
||||
display: "flex",
|
||||
gap: "0.5em",
|
||||
height: "fit-content",
|
||||
backgroundColor: "var(--color-primary) !important",
|
||||
border: "1px solid var(--color-base-40)",
|
||||
padding: "0.4em 0.5em",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "var(--font-ui-smaller)",
|
||||
fontWeight: "var(--font-normal)",
|
||||
}}
|
||||
>
|
||||
<Icon accent icon='square-check-big' />
|
||||
<span>Add Todo</span>
|
||||
</Button>
|
||||
<MenuComponent onMenuShow={onNoteMenuShow} />
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className={bem("tasks")}
|
||||
css={{
|
||||
height: "calc(100% - var(--header-height) - var(--header-padding-y) * 2)",
|
||||
}}
|
||||
>
|
||||
<TaskDndContext>
|
||||
{note.tasks.map(task => {
|
||||
switch(task.type){
|
||||
case "routine": return <RoutineTask key={task.name} task={task} />
|
||||
case "todo": return <TodoTask key={task.name} task={task} />
|
||||
default: task as never;
|
||||
}
|
||||
})}
|
||||
</TaskDndContext>
|
||||
</div>
|
||||
<header
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0.5em 1em",
|
||||
// height: "4.5em",
|
||||
}}
|
||||
className={bem("header")}
|
||||
>
|
||||
<span css={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "1.2em",
|
||||
}}>
|
||||
{note.day.getBaseFormat() + " / " + note.day.getDayOfWeek()}
|
||||
</span>
|
||||
<MenuComponent onMenuShow={onNoteMenuShow} icon="ellipsis" />
|
||||
</header>
|
||||
<div
|
||||
className={bem("tasks")}
|
||||
css={{
|
||||
flexGrow: 1,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<TaskDndContext>
|
||||
{note.tasks.map(task => {
|
||||
switch(task.type){
|
||||
case "routine": return <RoutineTask key={task.name} task={task} />
|
||||
case "todo": return <TodoTask key={task.name} task={task} />
|
||||
default: task as never;
|
||||
}
|
||||
})}
|
||||
</TaskDndContext>
|
||||
</div>
|
||||
<AddTodoModal />
|
||||
<StartRoutineModal />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import React, { useEffect, useRef } from "react";
|
|||
interface IconProps {
|
||||
icon: string;
|
||||
accent?: boolean;
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
export const Icon = React.memo((props: IconProps) => {
|
||||
|
|
@ -25,11 +26,14 @@ export const Icon = React.memo((props: IconProps) => {
|
|||
}, [props]);
|
||||
|
||||
return (
|
||||
<span
|
||||
<span
|
||||
className={props.className}
|
||||
ref={ref}
|
||||
ref={ref}
|
||||
css={{
|
||||
color: props.accent ? "var(--interactive-accent)" : "inherit",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: props.accent ? "var(--interactive-accent)" : props.color,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,27 +10,30 @@ import { Icon } from "./Icon";
|
|||
|
||||
interface MenuComponentProps {
|
||||
onMenuShow: (menu: Menu) => void;
|
||||
icon?: string;
|
||||
iconAccent?: boolean;
|
||||
}
|
||||
export const MenuComponent = (props: MenuComponentProps) => {
|
||||
export const MenuComponent = ({
|
||||
onMenuShow,
|
||||
icon = "menu",
|
||||
iconAccent = false
|
||||
}: MenuComponentProps) => {
|
||||
|
||||
const openMenu = useCallback((e: React.MouseEvent) => {
|
||||
const m = new Menu();
|
||||
props.onMenuShow(m);
|
||||
onMenuShow(m);
|
||||
m.showAtMouseEvent(e.nativeEvent);
|
||||
}, [props]);
|
||||
}, [onMenuShow]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={openMenu}
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Icon icon="menu" />
|
||||
<Icon icon={icon} accent={iconAccent} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
const cleanPercentage = (percentage: number) => {
|
||||
|
|
@ -40,12 +41,14 @@ interface PercentageCircleProps {
|
|||
percentage: number;
|
||||
text: string;
|
||||
transition?: boolean;
|
||||
width?: string;
|
||||
className?: string;
|
||||
}
|
||||
export const PercentageCircle = React.memo(({ percentage, text, transition=true }: PercentageCircleProps) => {
|
||||
export const PercentageCircle = React.memo(({ percentage, width, className, text, transition=true }: PercentageCircleProps) => {
|
||||
const pct = cleanPercentage(percentage);
|
||||
const color = pct > 70 ? "var(--color-accent-1)" : "var(--color-accent-2)";
|
||||
return (
|
||||
<svg width={"100%"} height={"100%"} data-percentage={percentage} viewBox="0 0 200 200" className="dr-percentage-circle">
|
||||
<svg width={width??"100%"} data-percentage={percentage} viewBox="0 0 200 200" className={clsx(className, "dr-percentage-circle")}>
|
||||
<g transform="rotate(-90 100 100)" >
|
||||
<Circle transition={false} color="#ececec" percentage={100} />
|
||||
<Circle transition={transition} color={color} percentage={pct} />
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ interface TouchableProps {
|
|||
// pressed 상태가 변경될 때 호출
|
||||
onChangePressedState?: (isPressed: boolean) => void;
|
||||
|
||||
// click인지, long press인지 판단하기 위한 지연시간(default: 200ms)
|
||||
// click인지, long press인지 판단하기 위한 지연시간
|
||||
longPressStartDelay?: number;
|
||||
|
||||
// long press가 시작될 때 호출
|
||||
|
|
@ -43,7 +43,7 @@ export const Touchable = memo(forwardRef<HTMLDivElement, TouchableProps>((props:
|
|||
|
||||
const clickPreventRef = useRef<boolean>(false);
|
||||
const performanceRef = useRef<number>(0);
|
||||
const startDelay = useMemo(() => props.longPressStartDelay??100, [props.longPressStartDelay]);
|
||||
const startDelay = useMemo(() => props.longPressStartDelay??150, [props.longPressStartDelay]);
|
||||
const longPressDelay = useMemo(() => props.longPressDelay??500, [props.longPressDelay]);
|
||||
const isMovedRef = useRef<boolean>(false);
|
||||
const isTouchRef = useRef<boolean>(false);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ export class Day {
|
|||
return this.#moment.isSame(moment(), 'day');
|
||||
}
|
||||
|
||||
isBefore(day: Day){
|
||||
return this.#moment.isBefore(day.#moment);
|
||||
}
|
||||
|
||||
isAfter(day: Day){
|
||||
return this.#moment.isAfter(day.#moment);
|
||||
}
|
||||
|
||||
add(amount: number, unit: moment.unitOfTime.DurationConstructor){
|
||||
this.#moment.add(amount, unit);
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 모바일에서 드래그를 시작하기 위한 딜레이 시간
|
||||
*/
|
||||
export const DRAG_PRESS_DELAY = 500;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
/** @jsxImportSource @emotion/react */
|
||||
import { Task } from "entities/note";
|
||||
import React, { CSSProperties, useEffect, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { TouchBackend } from "react-dnd-touch-backend";
|
||||
import {
|
||||
|
|
@ -11,7 +10,6 @@ import {
|
|||
createTransition,
|
||||
} from "react-dnd-multi-backend";
|
||||
import { Preview } from "react-dnd-preview";
|
||||
import { DRAG_PRESS_DELAY } from "./constants";
|
||||
import { TaskPreview } from "./ui/TaskPreview";
|
||||
import { useDndScroll } from "./use-dnd-scroll";
|
||||
import { useDragDropManager } from "react-dnd";
|
||||
|
|
@ -49,7 +47,7 @@ export const TaskDndContext = ({ children }: {children: React.ReactNode }) => {
|
|||
backend: TouchBackend,
|
||||
options: {
|
||||
enableMouseEvents: false,
|
||||
delayTouchStart: DRAG_PRESS_DELAY,
|
||||
delayTouchStart: 500,
|
||||
ignoreContextMenu: false
|
||||
},
|
||||
transition: TouchTransition
|
||||
|
|
@ -99,10 +97,11 @@ const ScrollComponent = ({ children }: ScrollComponentProps) => {
|
|||
return (
|
||||
<div
|
||||
ref={listRef}
|
||||
className="dr-note__task-scroll"
|
||||
className="dr-scroll-container"
|
||||
css={{
|
||||
height: "100%",
|
||||
overflowY: "auto",
|
||||
padding: "0 10px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { RoutineNote, routineNoteArchiver, routineNoteService, Task as TaskEntit
|
|||
import React, { useCallback, useMemo, useRef, useState } from "react"
|
||||
import { useRoutineNote } from "entities/note";
|
||||
import _ from "lodash";
|
||||
import { DRAG_PRESS_DELAY } from "../constants";
|
||||
import { Touchable } from 'shared/components/Touchable';
|
||||
import { dr } from 'shared/daily-routine-bem';
|
||||
import { Icon } from 'shared/components/Icon';
|
||||
|
|
@ -11,7 +10,6 @@ import { TaskName } from './TaskName';
|
|||
import { Checkbox } from './Checkbox';
|
||||
import { css } from '@emotion/react';
|
||||
import { useTaskDnd } from '../hooks/use-task-dnd';
|
||||
import { moment } from 'obsidian';
|
||||
|
||||
|
||||
// [[ Styles
|
||||
|
|
@ -26,6 +24,7 @@ const taskPressedAndDraggingStyle = css({
|
|||
height: "100%",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "hsla(var(--color-accent-1-hsl), 0.5)",
|
||||
transition: "background-color 0.5s",
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -128,12 +127,15 @@ export const AbstractTask = React.memo(<T extends TaskEntity>({ className, task,
|
|||
touchAction: "manipulation",
|
||||
userSelect: "none",
|
||||
},
|
||||
"&::after": {
|
||||
content: "''",
|
||||
},
|
||||
"&.dr-task--pressed, &.dr-task--dragging": taskPressedAndDraggingStyle
|
||||
}}
|
||||
>
|
||||
<Touchable
|
||||
onClick={onClick}
|
||||
longPressDelay={DRAG_PRESS_DELAY}
|
||||
longPressDelay={500}
|
||||
onChangePressedState={setIsPressed}
|
||||
onLongPressStart={onLongPressStart}
|
||||
onAfterLongPressDelay={onAfterLongPressDelay}
|
||||
|
|
@ -168,7 +170,7 @@ export const AbstractTask = React.memo(<T extends TaskEntity>({ className, task,
|
|||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Icon icon="menu" />
|
||||
<Icon icon="menu" color='var(--color-base-40)' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
34
styles.css
34
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue