해방 탭 상태 분리 (제네시스/데스티니 각각)

- Liberation.jsx를 탭 shell로 단순화하고 Genesis/Destiny 컴포넌트 분리
- liberationType을 store에 persist해 새로고침/재접속 후에도 마지막 탭 유지
- calcMode를 genesisCalcMode/destinyCalcMode로 분리해 무기별 독립 저장
- ProgressBar를 chapters/imageBase/completionColor prop 받도록 일반화
- Destiny 컴포넌트에 계산 모드 탭 + 진행 바 표시, 완료일 색은 sky blue
  (--destiny-date: 다크 #38bdf8 / 라이트 #0284c7)
- 데스티니 전용 슬롯(destinySimple/destinyWeekly)과 updateDestinySlot/
  resetDestinySlot 액션 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-21 19:03:52 +09:00
parent 29fcb39eb3
commit 0448b0bfc8
6 changed files with 346 additions and 255 deletions

View file

@ -0,0 +1,54 @@
import { DESTINY_CHAPTERS, DESTINY_QUEST_IMAGE_BASE } from '../data'
import { useLiberationStore } from '../store'
import ProgressBar from './components/ProgressBar'
export default function Destiny() {
const calcMode = useLiberationStore((s) => s.destinyCalcMode)
const setCalcMode = useLiberationStore((s) => s.setDestinyCalcMode)
const state = useLiberationStore((s) => s.destinyCalcMode === 'weekly' ? s.destinyWeekly : s.destinySimple)
return (
<>
{/* 계산 모드 탭 */}
<div
className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border"
style={{
background: 'var(--surface-3)',
borderColor: 'var(--panel-border)',
}}
>
{[
{ key: 'simple', label: '단순 계산' },
{ key: 'weekly', label: '주차별 계산' },
].map((t) => {
const active = calcMode === t.key
return (
<button
key={t.key}
type="button"
onClick={() => setCalcMode(t.key)}
className="flex-1 h-10 rounded-lg text-sm font-semibold"
style={active ? {
background: 'var(--selected-bg)',
color: 'var(--accent-bright)',
} : {
color: 'var(--text-muted)',
}}
>
{t.label}
</button>
)
})}
</div>
<ProgressBar
chapters={DESTINY_CHAPTERS}
imageBase={DESTINY_QUEST_IMAGE_BASE}
startChapter={state.startChapter}
currentPoints={state.currentPoints}
completionDate={null}
completionColor="var(--destiny-date)"
/>
</>
)
}

View file

@ -0,0 +1,234 @@
import { useState, useMemo } from 'react'
import dayjs from 'dayjs'
import {
GENESIS_CHAPTERS,
GENESIS_TOTAL,
MONTHLY_BOSSES,
QUEST_BOSS_IMAGE_BASE,
formatDate,
} from '../data'
import { useLiberationStore } from '../store'
import {
bossEarn,
calcWeekPoints,
calcDoneEarn,
calcMonthlyEarn,
getSchedulerWeekRange,
computeCompletionDate,
} from '../utils'
import QuestSelector from './components/QuestSelector'
import PointsInput from './components/PointsInput'
import ProgressBar from './components/ProgressBar'
import WeeklyDefault from './components/WeeklyDefault'
import DatePicker from '../../../components/common/DatePicker'
import ConfirmDialog from '../../../components/common/ConfirmDialog'
export default function Genesis() {
const calcMode = useLiberationStore((s) => s.genesisCalcMode)
const state = useLiberationStore((s) => s[s.genesisCalcMode])
const setCalcMode = useLiberationStore((s) => s.setGenesisCalcMode)
const updateSlot = useLiberationStore((s) => s.updateSlot)
const resetSlot = useLiberationStore((s) => s.resetSlot)
const setState = (updater) => updateSlot(updater)
// : required
const priorConsumed = GENESIS_CHAPTERS
.slice(0, state.startChapter)
.reduce((s, c) => s + c.required, 0)
let cascadeIdx = state.startChapter
let cascadeRemain = state.currentPoints
let cascadeConsumed = 0
while (cascadeIdx < GENESIS_CHAPTERS.length && cascadeRemain >= GENESIS_CHAPTERS[cascadeIdx].required) {
cascadeConsumed += GENESIS_CHAPTERS[cascadeIdx].required
cascadeRemain -= GENESIS_CHAPTERS[cascadeIdx].required
cascadeIdx++
}
const initialAccumulated = priorConsumed + cascadeConsumed + cascadeRemain
const alreadyDone = initialAccumulated >= GENESIS_TOTAL
const weeklyEarn = calcWeekPoints(state.weekly)
const remaining = Math.max(GENESIS_TOTAL - initialAccumulated, 0)
const doneEarn = calcDoneEarn(state.weekly)
const monthlyEarn = calcMonthlyEarn(state.weekly)
const monthlyDoneThisMonth = !!state.weekly.blackMage?.done
// ( 1 )
const headerWeekly = calcMode === 'weekly'
? (state.schedulerWeeks || []).reduce((s, w) => s + calcWeekPoints(w.config), 0)
: weeklyEarn
const headerMonthly = (() => {
if (calcMode !== 'weekly') return monthlyEarn
const sw = state.schedulerWeeks || []
if (!state.startDate) return 0
const claimed = {}
sw.forEach((w, idx) => {
const diff = w.config.blackMage?.difficulty
if (!diff || diff === 'none') return
const r = getSchedulerWeekRange(state.startDate, idx + 1)
const months = [r.start.format('YYYY-MM'), r.end.format('YYYY-MM')]
for (const m of months) {
if (!(m in claimed)) {
claimed[m] = bossEarn(MONTHLY_BOSSES[0], w.config.blackMage)
return
}
}
})
return Object.values(claimed).reduce((s, v) => s + v, 0)
})()
const completionDate = useMemo(
() => computeCompletionDate({
calcMode, state, alreadyDone, remaining,
weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth,
}),
[calcMode, state, alreadyDone, remaining, weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth],
)
const isDone = completionDate !== null
const [resetOpen, setResetOpen] = useState(false)
const doReset = () => {
resetSlot()
setResetOpen(false)
}
return (
<>
{/* 계산 모드 탭 */}
<div
className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border"
style={{
background: 'var(--surface-3)',
borderColor: 'var(--panel-border)',
}}
>
{[
{ key: 'simple', label: '단순 계산' },
{ key: 'weekly', label: '주차별 계산' },
].map((t) => {
const active = calcMode === t.key
return (
<button
key={t.key}
type="button"
onClick={() => setCalcMode(t.key)}
className="flex-1 h-10 rounded-lg text-sm font-semibold"
style={active ? {
background: 'var(--selected-bg)',
color: 'var(--accent-bright)',
} : {
color: 'var(--text-muted)',
}}
>
{t.label}
</button>
)
})}
</div>
<ProgressBar
chapters={GENESIS_CHAPTERS}
imageBase={QUEST_BOSS_IMAGE_BASE}
startChapter={state.startChapter}
currentPoints={state.currentPoints}
completionDate={isDone ? formatDate(completionDate) : null}
/>
{/* 현재 진행 상태 입력 */}
<div
className="max-w-3xl mx-auto rounded-2xl border p-6 space-y-4"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--panel-border)',
boxShadow: 'var(--panel-shadow)',
}}
>
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>현재 진행 상태</div>
<div className="grid gap-3 grid-cols-3">
<div className="space-y-1.5">
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>시작 날짜</label>
<DatePicker
value={formatDate(state.startDate)}
onChange={(d) => setState((prev) => ({ ...prev, startDate: dayjs(d).toISOString() }))}
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>진행 중인 퀘스트</label>
<QuestSelector
value={state.startChapter}
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>현재 흔적</label>
<div
className="flex items-stretch rounded-lg border focus-within:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
style={{
background: 'var(--input-bg)',
borderColor: 'var(--input-border)',
}}
>
<PointsInput
value={state.currentPoints}
max={3000}
onChange={(n) => setState((prev) => ({ ...prev, currentPoints: n }))}
className="flex-1 min-w-0 bg-transparent px-3 h-12 text-base text-right tabular-nums outline-none"
style={{ color: 'var(--text-strong)' }}
/>
<span
className="flex items-center px-3 text-base border-l select-none tabular-nums"
style={{
borderColor: 'var(--input-border)',
color: 'var(--text-dim)',
}}
>
/ {(GENESIS_CHAPTERS[state.startChapter]?.required ?? 0).toLocaleString()}
</span>
</div>
</div>
</div>
</div>
<WeeklyDefault
weekly={state.weekly}
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
totalWeekly={headerWeekly}
totalMonthly={headerMonthly}
remaining={remaining}
mode={calcMode}
startDate={state.startDate}
weeks={state.schedulerWeeks}
onChangeWeeks={(w) => setState((prev) => ({ ...prev, schedulerWeeks: w }))}
/>
<div className="max-w-3xl mx-auto flex justify-end">
<button
type="button"
onClick={() => setResetOpen(true)}
className="inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-semibold hover:bg-[var(--danger-bg-hover)]"
style={{
borderColor: 'var(--icon-danger-border)',
background: 'var(--icon-danger-bg)',
color: 'var(--danger-text)',
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 3H14M6 3V2C6 1.45 6.45 1 7 1H9C9.55 1 10 1.45 10 2V3M3 3L4 14C4 14.55 4.45 15 5 15H11C11.55 15 12 14.55 12 14L13 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
전체 초기화
</button>
</div>
<ConfirmDialog
open={resetOpen}
onClose={() => setResetOpen(false)}
onConfirm={doReset}
title="전체 초기화"
description={`${calcMode === 'simple' ? '단순 계산' : '주차별 계산'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`}
confirmText="초기화"
destructive
/>
</>
)
}

View file

@ -1,29 +1,10 @@
import { useState, useLayoutEffect, useMemo } from 'react'
import { useLayoutEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { api } from '../../../api/client'
import {
GENESIS_CHAPTERS,
GENESIS_TOTAL,
MONTHLY_BOSSES,
formatDate,
} from '../data'
import { useLiberationStore } from '../store'
import {
bossEarn,
calcWeekPoints,
calcDoneEarn,
calcMonthlyEarn,
getSchedulerWeekRange,
computeCompletionDate,
} from '../utils'
import QuestSelector from './components/QuestSelector'
import PointsInput from './components/PointsInput'
import ProgressBar from './components/ProgressBar'
import WeeklyDefault from './components/WeeklyDefault'
import DatePicker from '../../../components/common/DatePicker'
import ConfirmDialog from '../../../components/common/ConfirmDialog'
import { useLayout } from '../../../components/pc/Layout'
import { useLiberationStore } from '../store'
import Genesis from './Genesis'
import Destiny from './Destiny'
export default function Liberation() {
const { setFullscreen } = useLayout()
@ -32,7 +13,8 @@ export default function Liberation() {
return () => setFullscreen(false)
}, [setFullscreen])
const [liberationType, setLiberationType] = useState('genesis') // 'genesis' | 'destiny'
const liberationType = useLiberationStore((s) => s.liberationType)
const setLiberationType = useLiberationStore((s) => s.setLiberationType)
const genesisImg = useQuery({
queryKey: ['image', '제네시스 스태프'],
@ -45,72 +27,6 @@ export default function Liberation() {
staleTime: Infinity,
})
const calcMode = useLiberationStore((s) => s.calcMode)
const state = useLiberationStore((s) => s[s.calcMode])
const setCalcMode = useLiberationStore((s) => s.setCalcMode)
const updateSlot = useLiberationStore((s) => s.updateSlot)
const resetSlot = useLiberationStore((s) => s.resetSlot)
const setState = (updater) => updateSlot(updater)
// : required
const priorConsumed = GENESIS_CHAPTERS
.slice(0, state.startChapter)
.reduce((s, c) => s + c.required, 0)
let cascadeIdx = state.startChapter
let cascadeRemain = state.currentPoints
let cascadeConsumed = 0
while (cascadeIdx < GENESIS_CHAPTERS.length && cascadeRemain >= GENESIS_CHAPTERS[cascadeIdx].required) {
cascadeConsumed += GENESIS_CHAPTERS[cascadeIdx].required
cascadeRemain -= GENESIS_CHAPTERS[cascadeIdx].required
cascadeIdx++
}
const initialAccumulated = priorConsumed + cascadeConsumed + cascadeRemain
const alreadyDone = initialAccumulated >= GENESIS_TOTAL
const weeklyEarn = calcWeekPoints(state.weekly)
const remaining = Math.max(GENESIS_TOTAL - initialAccumulated, 0)
const doneEarn = calcDoneEarn(state.weekly)
const monthlyEarn = calcMonthlyEarn(state.weekly)
const monthlyDoneThisMonth = !!state.weekly.blackMage?.done
// ( 1 )
const headerWeekly = calcMode === 'weekly'
? (state.schedulerWeeks || []).reduce((s, w) => s + calcWeekPoints(w.config), 0)
: weeklyEarn
const headerMonthly = (() => {
if (calcMode !== 'weekly') return monthlyEarn
const sw = state.schedulerWeeks || []
if (!state.startDate) return 0
const claimed = {}
sw.forEach((w, idx) => {
const diff = w.config.blackMage?.difficulty
if (!diff || diff === 'none') return
const r = getSchedulerWeekRange(state.startDate, idx + 1)
const months = [r.start.format('YYYY-MM'), r.end.format('YYYY-MM')]
for (const m of months) {
if (!(m in claimed)) {
claimed[m] = bossEarn(MONTHLY_BOSSES[0], w.config.blackMage)
return
}
}
})
return Object.values(claimed).reduce((s, v) => s + v, 0)
})()
const completionDate = useMemo(
() => computeCompletionDate({
calcMode, state, alreadyDone, remaining,
weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth,
}),
[calcMode, state, alreadyDone, remaining, weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth],
)
const isDone = completionDate !== null
const [resetOpen, setResetOpen] = useState(false)
const doReset = () => {
resetSlot()
setResetOpen(false)
}
return (
<div className="space-y-6 pb-10">
{/* 해방 종류 탭 */}
@ -144,156 +60,7 @@ export default function Liberation() {
})}
</div>
{liberationType === 'destiny' ? (
<div
className="max-w-3xl mx-auto rounded-2xl border p-16 text-center space-y-3 flex flex-col items-center justify-center"
style={{
minHeight: 'calc(100vh - 220px)',
background: 'var(--panel-bg)',
borderColor: 'var(--panel-border)',
boxShadow: 'var(--panel-shadow)',
}}
>
<div className="text-2xl font-bold" style={{ color: 'var(--text-emphasis)' }}>구현 예정</div>
<div className="text-sm" style={{ color: 'var(--text-dim)' }}>데스티니 해방 계산기는 준비 중입니다.</div>
</div>
) : (<>
{/* 계산 모드 탭 */}
<div
className="max-w-3xl mx-auto flex gap-1 p-1 rounded-xl border"
style={{
background: 'var(--surface-3)',
borderColor: 'var(--panel-border)',
}}
>
{[
{ key: 'simple', label: '단순 계산' },
{ key: 'weekly', label: '주차별 계산' },
].map((t) => {
const active = calcMode === t.key
return (
<button
key={t.key}
type="button"
onClick={() => setCalcMode(t.key)}
className="flex-1 h-10 rounded-lg text-sm font-semibold"
style={active ? {
background: 'var(--selected-bg)',
color: 'var(--accent-bright)',
} : {
color: 'var(--text-muted)',
}}
>
{t.label}
</button>
)
})}
</div>
<ProgressBar
startChapter={state.startChapter}
currentPoints={state.currentPoints}
completionDate={isDone ? formatDate(completionDate) : null}
/>
{/* 현재 진행 상태 입력 */}
<div
className="max-w-3xl mx-auto rounded-2xl border p-6 space-y-4"
style={{
background: 'var(--panel-bg)',
borderColor: 'var(--panel-border)',
boxShadow: 'var(--panel-shadow)',
}}
>
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>현재 진행 상태</div>
<div className="grid gap-3 grid-cols-3">
<div className="space-y-1.5">
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>시작 날짜</label>
<DatePicker
value={formatDate(state.startDate)}
onChange={(d) => setState((prev) => ({ ...prev, startDate: dayjs(d).toISOString() }))}
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>진행 중인 퀘스트</label>
<QuestSelector
value={state.startChapter}
onChange={(idx) => setState((prev) => ({ ...prev, startChapter: idx }))}
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs" style={{ color: 'var(--text-muted)' }}>현재 흔적</label>
<div
className="flex items-stretch rounded-lg border focus-within:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
style={{
background: 'var(--input-bg)',
borderColor: 'var(--input-border)',
}}
>
<PointsInput
value={state.currentPoints}
max={3000}
onChange={(n) => setState((prev) => ({ ...prev, currentPoints: n }))}
className="flex-1 min-w-0 bg-transparent px-3 h-12 text-base text-right tabular-nums outline-none"
style={{ color: 'var(--text-strong)' }}
/>
<span
className="flex items-center px-3 text-base border-l select-none tabular-nums"
style={{
borderColor: 'var(--input-border)',
color: 'var(--text-dim)',
}}
>
/ {(GENESIS_CHAPTERS[state.startChapter]?.required ?? 0).toLocaleString()}
</span>
</div>
</div>
</div>
</div>
<WeeklyDefault
weekly={state.weekly}
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
totalWeekly={headerWeekly}
totalMonthly={headerMonthly}
remaining={remaining}
mode={calcMode}
startDate={state.startDate}
weeks={state.schedulerWeeks}
onChangeWeeks={(w) => setState((prev) => ({ ...prev, schedulerWeeks: w }))}
/>
<div className="max-w-3xl mx-auto flex justify-end">
<button
type="button"
onClick={() => setResetOpen(true)}
className="inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-semibold hover:bg-[var(--danger-bg-hover)]"
style={{
borderColor: 'var(--icon-danger-border)',
background: 'var(--icon-danger-bg)',
color: 'var(--danger-text)',
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 3H14M6 3V2C6 1.45 6.45 1 7 1H9C9.55 1 10 1.45 10 2V3M3 3L4 14C4 14.55 4.45 15 5 15H11C11.55 15 12 14.55 12 14L13 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
전체 초기화
</button>
</div>
</>)}
<ConfirmDialog
open={resetOpen}
onClose={() => setResetOpen(false)}
onConfirm={doReset}
title="전체 초기화"
description={`${calcMode === 'simple' ? '단순 계산' : '주차별 계산'} 모드의 입력을 모두 초기화하시겠습니까?\n\n시작 날짜, 현재 진행 상태, 주간 보스 설정이 모두 초기값으로 되돌아갑니다.\n다른 모드의 값은 유지됩니다.`}
confirmText="초기화"
destructive
/>
{liberationType === 'genesis' ? <Genesis /> : <Destiny />}
</div>
)
}

View file

@ -1,5 +1,3 @@
import { GENESIS_CHAPTERS, GENESIS_TOTAL, QUEST_BOSS_IMAGE_BASE } from '../../data'
const DOW = ['일', '월', '화', '수', '목', '금', '토']
function formatKoreanDate(s) {
const [y, m, d] = s.split('-')
@ -7,8 +5,15 @@ function formatKoreanDate(s) {
return `${y}${m}${d}일 (${dow})`
}
export default function ProgressBar({ startChapter, currentPoints, completionDate }) {
const chapterStates = GENESIS_CHAPTERS.map((c) => {
export default function ProgressBar({
chapters,
imageBase,
startChapter,
currentPoints,
completionDate,
completionColor = 'var(--warning-text-bright)',
}) {
const chapterStates = chapters.map((c) => {
if (c.idx < startChapter) return { chapter: c, status: 'done', current: c.required }
if (c.idx === startChapter) {
const filled = Math.min(currentPoints, c.required)
@ -41,7 +46,7 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
status === 'pending' ? 'opacity-50' : ''
}`}>
<img
src={`${QUEST_BOSS_IMAGE_BASE}/${chapter.boss}.webp`}
src={`${imageBase}/${chapter.boss}.webp`}
alt={chapter.boss}
className={`block w-full h-full object-cover ${status === 'pending' ? 'grayscale' : ''}`}
/>
@ -101,7 +106,7 @@ export default function ProgressBar({ startChapter, currentPoints, completionDat
<span style={{ color: 'var(--text-dim)' }}>·</span>
<span
className="text-xl font-bold tabular-nums"
style={{ color: 'var(--warning-text-bright)' }}
style={{ color: completionColor }}
>
{completionDate ? formatKoreanDate(completionDate) : <span className="font-normal" style={{ color: 'var(--text-dim)' }}>미정</span>}
</span>

View file

@ -24,28 +24,55 @@ function makeInitialSlot() {
}
}
function makeInitialDestinySlot() {
return {
startChapter: 0,
currentPoints: 0,
startDate: dayjs(todayKST()).toISOString(),
}
}
/**
* 해방 계산기 상태
* calcMode: 'simple' | 'weekly'
* simple / weekly: 모드 독립 슬롯
* calcMode: 'simple' | 'weekly' (제네시스/데스티니가 공유)
* simple / weekly: 제네시스 모드별 독립 슬롯
* destinySimple / destinyWeekly: 데스티니 모드별 독립 슬롯
*/
export const useLiberationStore = create(persist(
(set) => ({
calcMode: 'simple',
liberationType: 'genesis', // 'genesis' | 'destiny'
genesisCalcMode: 'simple',
destinyCalcMode: 'simple',
simple: makeInitialSlot(),
weekly: makeInitialSlot(),
destinySimple: makeInitialDestinySlot(),
destinyWeekly: makeInitialDestinySlot(),
setCalcMode: (mode) => set({ calcMode: mode }),
setLiberationType: (type) => set({ liberationType: type }),
setGenesisCalcMode: (mode) => set({ genesisCalcMode: mode }),
setDestinyCalcMode: (mode) => set({ destinyCalcMode: mode }),
updateSlot: (patch) => set((s) => ({
[s.calcMode]: typeof patch === 'function'
? patch(s[s.calcMode])
: { ...s[s.calcMode], ...patch },
[s.genesisCalcMode]: typeof patch === 'function'
? patch(s[s.genesisCalcMode])
: { ...s[s.genesisCalcMode], ...patch },
})),
resetSlot: () => set((s) => ({ [s.calcMode]: makeInitialSlot() })),
resetSlot: () => set((s) => ({ [s.genesisCalcMode]: makeInitialSlot() })),
updateDestinySlot: (patch) => set((s) => {
const key = s.destinyCalcMode === 'weekly' ? 'destinyWeekly' : 'destinySimple'
return {
[key]: typeof patch === 'function' ? patch(s[key]) : { ...s[key], ...patch },
}
}),
resetDestinySlot: () => set((s) => {
const key = s.destinyCalcMode === 'weekly' ? 'destinyWeekly' : 'destinySimple'
return { [key]: makeInitialDestinySlot() }
}),
}),
{ name: 'maple-liberation' },
))
export { makeEmptyWeekly, makeInitialSlot }
export { makeEmptyWeekly, makeInitialSlot, makeInitialDestinySlot }

View file

@ -105,6 +105,8 @@
--warning-text-bright: #fcd34d;
--warning-text-dim: rgba(252, 211, 77, 0.4);
--destiny-date: #38bdf8;
--progress-track: #0f172a;
--progress-emerald: #10b981;
--progress-amber: #f59e0b;
@ -256,6 +258,8 @@
--warning-text-bright: #ea580c;
--warning-text-dim: rgba(234, 88, 12, 0.4);
--destiny-date: #0284c7;
--progress-track: #e5e7eb;
--progress-emerald: #10b981;
--progress-amber: #f59e0b;