fix: ui interactions — subtask detail, calendar events, modal prompts, search visibility
ci / docker (push) Successful in 3m23s
ci / docker (push) Successful in 3m23s
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f15a29"><link rel="manifest" href="/manifest.json"><title>dodo</title> <script type="module" crossorigin src="/assets/index-BqVyvsa4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BBuUKFZL.css">
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f15a29"><link rel="manifest" href="/manifest.json"><title>dodo</title> <script type="module" crossorigin src="/assets/index-COed2nq9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dhk-VG-k.css">
|
||||
</head><body><div id="app"></div></body></html>
|
||||
+81
-18
@@ -6,7 +6,7 @@ import {
|
||||
Settings, Trash2, X, Repeat2,
|
||||
} from 'lucide-vue-next'
|
||||
import { filterTasks, fromDateTimeLocal, groupTaskTree, renderMarkdown, toDateTimeLocal } from './lib/task-utils'
|
||||
import { defaultView, isTaskView, shouldToggleRowSwipe } from './lib/mvp-utils'
|
||||
import { dateKey, defaultView, isTaskView, shouldToggleRowSwipe } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
import MvpPanel from './MvpPanel.vue'
|
||||
|
||||
@@ -15,6 +15,7 @@ type TaskList = { id: string; folder_id: string | null; name: string; is_inbox:
|
||||
type CalendarSubscription = { id: string; name: string; url: string; color?: string }
|
||||
type Tag = { id: string; name: string; color: string }
|
||||
type Task = { id: string; list_id: string; parent_id: string | null; title: string; description: string; priority: number; completed: boolean; version: number; due_at: string | null; recurrence_rule?: string | null; recurrence_end_at?: string | null; tags?: Tag[]; subtasks?: Task[] }
|
||||
type CalendarEvent = { id: string; title: string; starts_at: string; ends_at?: string | null; all_day: boolean; source_name: string; color?: string }
|
||||
type View = 'tasks' | 'today' | 'upcoming' | 'trash' | 'habits' | 'calendar' | 'settings'
|
||||
|
||||
const initialized = ref<boolean | null>(null)
|
||||
@@ -26,6 +27,7 @@ const folders = ref<FolderItem[]>([])
|
||||
const lists = ref<TaskList[]>([])
|
||||
const archivedLists = ref<TaskList[]>([])
|
||||
const calendarSubscriptions = ref<CalendarSubscription[]>([])
|
||||
const calendarEvents = ref<CalendarEvent[]>([])
|
||||
const calendarName = ref('合并日历')
|
||||
const calendarUrl = ref('https://cal.bboy.app/all.ics')
|
||||
const tags = ref<Tag[]>([])
|
||||
@@ -53,6 +55,31 @@ const taskSwipeStart = ref<{ id: string; x: number; y: number } | null>(null)
|
||||
const taskSwipeOffsets = ref<Record<string, number>>({})
|
||||
let suppressTaskClickId = ''
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const modalLabel = ref('')
|
||||
const modalValue = ref('')
|
||||
const modalConfirmText = ref('确定')
|
||||
const modalResolve = ref<((value: string | null) => void) | null>(null)
|
||||
function askText(title: string, label = '', initial = '', confirmText = '确定') {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
modalTitle.value = title
|
||||
modalLabel.value = label
|
||||
modalValue.value = initial
|
||||
modalConfirmText.value = confirmText
|
||||
modalVisible.value = true
|
||||
modalResolve.value = resolve
|
||||
})
|
||||
}
|
||||
function closeModal() {
|
||||
modalVisible.value = false
|
||||
if (modalResolve.value) { modalResolve.value(null); modalResolve.value = null }
|
||||
}
|
||||
function confirmModal() {
|
||||
modalVisible.value = false
|
||||
if (modalResolve.value) { modalResolve.value(modalValue.value); modalResolve.value = null }
|
||||
}
|
||||
|
||||
const activeName = computed(() => {
|
||||
if (activeView.value === 'trash') return '回收站'
|
||||
if (activeView.value === 'today') return '今天'
|
||||
@@ -67,11 +94,12 @@ const visibleTasks = computed(() => {
|
||||
const now = new Date()
|
||||
const end = new Date(now); end.setDate(end.getDate() + 7)
|
||||
let result = sourceTasks.value
|
||||
if (['habits','settings'].includes(activeView.value)) return []
|
||||
if (['habits','settings','calendar'].includes(activeView.value)) return []
|
||||
if (activeView.value === 'today') result = result.filter((task) => task.due_at && new Date(task.due_at).toDateString() === now.toDateString())
|
||||
if (activeView.value === 'upcoming') result = result.filter((task) => task.due_at && new Date(task.due_at) >= now && new Date(task.due_at) <= end)
|
||||
return query.value.trim() ? filterTasks(result, query.value) : result
|
||||
})
|
||||
const selectedTaskSubtasks = computed(() => selectedTask.value?.subtasks ?? [])
|
||||
const filteredTaskTree = computed(() => groupTaskTree(visibleTasks.value))
|
||||
const taskTree = computed(() => filteredTaskTree.value)
|
||||
let searchTimer: number | undefined
|
||||
@@ -219,6 +247,7 @@ async function switchView(view: View, listId?: string) {
|
||||
selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false
|
||||
if (view === 'trash') await loadTrash()
|
||||
else if (view === 'calendar') await loadCalendarTab()
|
||||
else if (view === 'today') await loadTodayView()
|
||||
else if (!isTaskView(view)) tasks.value = []
|
||||
else await loadAll()
|
||||
}
|
||||
@@ -229,6 +258,9 @@ async function addTask() {
|
||||
tasks.value.push(task); title.value = ''; selectTask(task); toast('任务已添加')
|
||||
} catch (reason) { fail(reason) }
|
||||
}
|
||||
async function loadTodayView() {
|
||||
await Promise.all([loadAll(), loadTodayEvents()])
|
||||
}
|
||||
async function patchTask(task: Task, patch: Partial<Task>) {
|
||||
const updated = await api(`/tasks/${task.id}`, { method: 'PATCH', body: JSON.stringify({ ...patch, version: task.version }) })
|
||||
const index = tasks.value.findIndex((item) => item.id === task.id)
|
||||
@@ -312,33 +344,40 @@ async function purgeTask(task: Task) {
|
||||
}
|
||||
async function addSubtask() {
|
||||
if (!selectedTask.value) return
|
||||
const subtaskTitle = window.prompt('子任务名称')?.trim()
|
||||
const subtaskTitle = (await askText('添加子任务', '子任务名称', '', '添加'))?.trim()
|
||||
if (!subtaskTitle) return
|
||||
try { const child = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: subtaskTitle, list_id: selectedTask.value.list_id, parent_id: selectedTask.value.id }) }); tasks.value.push(child); toast('子任务已添加') } catch (reason) { fail(reason) }
|
||||
try { const child = await api('/tasks', { method: 'POST', body: JSON.stringify({ title: subtaskTitle, list_id: selectedTask.value.list_id, parent_id: selectedTask.value.id }) }); if (selectedTask.value) selectedTask.value.subtasks = [...(selectedTask.value.subtasks ?? []), child]; tasks.value.push(child); toast('子任务已添加') } catch (reason) { fail(reason) }
|
||||
}
|
||||
function selectTask(task: Task) {
|
||||
selectedTask.value = { ...task, tags: task.tags ? [...task.tags] : [] }
|
||||
markdownPreview.value = false; mobileDetail.value = true
|
||||
}
|
||||
async function createFolder() {
|
||||
const name = window.prompt('文件夹名称')?.trim(); if (!name) return
|
||||
const name = (await askText('新建文件夹', '文件夹名称', '', '创建'))?.trim(); if (!name) return
|
||||
try { folders.value.push(await api('/folders', { method: 'POST', body: JSON.stringify({ name }) })); toast('文件夹已创建') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function createList(folderId: string | null = null) {
|
||||
const name = window.prompt('清单名称')?.trim(); if (!name) return
|
||||
const name = (await askText('新建清单', '清单名称', '', '创建'))?.trim(); if (!name) return
|
||||
try { const item = await api('/lists', { method: 'POST', body: JSON.stringify({ name, folder_id: folderId }) }); lists.value.push(item); await switchView('tasks', item.id); toast('清单已创建') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function renameEntity(kind: 'folders' | 'lists', item: FolderItem | TaskList) {
|
||||
const name = window.prompt('新名称', item.name)?.trim(); if (!name || name === item.name) return
|
||||
const name = (await askText('重命名', kind === 'lists' ? '清单名称' : '文件夹名称', item.name, '保存'))?.trim(); if (!name || name === item.name) return
|
||||
try { const updated = await api(`/${kind}/${item.id}`, { method: 'PATCH', body: JSON.stringify({ name }) }); Object.assign(item, updated); toast('已重命名') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function createTag() {
|
||||
const name = (await askText('新建标签', '标签名称', '', '创建'))?.trim(); if (!name) return
|
||||
const color = (await askText('标签颜色', '颜色(十六进制)', '#F15A29', '创建'))?.trim() || '#F15A29'
|
||||
try { tags.value.push(await api('/tags', { method: 'POST', body: JSON.stringify({ name, color }) })); toast('标签已创建') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function deleteEntity(kind: 'folders' | 'lists', item: FolderItem | TaskList) {
|
||||
if (kind === 'lists') {
|
||||
if (!window.confirm(`归档“${item.name}”?任务会移到收集箱,历史数据保留。`)) return
|
||||
const answer = await askText(`归档清单「${item.name}」?`, '', '', '归档')
|
||||
if (answer === null) return
|
||||
try { await api(`/${kind}/${item.id}`, { method: 'DELETE' }); await loadArchivedLists(); await refreshAll(); toast('清单已归档') } catch (reason) { fail(reason) }
|
||||
return
|
||||
}
|
||||
if (!window.confirm(`删除“${item.name}”?`)) return
|
||||
const answer = await askText(`删除文件夹「${item.name}」?`, '', '', '删除')
|
||||
if (answer === null) return
|
||||
try { await api(`/${kind}/${item.id}`, { method: 'DELETE' }); await refreshAll(); toast('已删除') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function loadArchivedLists() {
|
||||
@@ -348,7 +387,14 @@ async function restoreList(item: TaskList) {
|
||||
try { await api(`/lists/${item.id}/restore`, { method: 'POST' }); await loadArchivedLists(); await refreshAll(); toast('清单已恢复') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function loadCalendarTab() {
|
||||
try { calendarSubscriptions.value = await api('/calendar-subscriptions') } catch { calendarSubscriptions.value = [] }
|
||||
try {
|
||||
calendarSubscriptions.value = await api('/calendar-subscriptions')
|
||||
if (calendarSubscriptions.value.length) {
|
||||
calendarEvents.value = await api(`/calendar-subscriptions/today?day=${dateKey(new Date())}`)
|
||||
} else {
|
||||
calendarEvents.value = []
|
||||
}
|
||||
} catch { calendarSubscriptions.value = []; calendarEvents.value = [] }
|
||||
}
|
||||
async function addCalendarSubscription() {
|
||||
if (!calendarUrl.value.trim()) return
|
||||
@@ -361,11 +407,6 @@ async function removeCalendarSubscription(id: string) {
|
||||
if (!window.confirm('取消这个日历订阅?')) return
|
||||
try { await api(`/calendar-subscriptions/${id}`, { method: 'DELETE' }); await loadCalendarTab(); toast('已取消订阅') } catch (reason) { fail(reason) }
|
||||
}
|
||||
async function createTag() {
|
||||
const name = window.prompt('标签名称')?.trim(); if (!name) return
|
||||
const color = window.prompt('标签颜色', '#F15A29') || '#F15A29'
|
||||
try { tags.value.push(await api('/tags', { method: 'POST', body: JSON.stringify({ name, color }) })); toast('标签已创建') } catch (reason) { fail(reason) }
|
||||
}
|
||||
function taskHasTag(tag: Tag) { return !!selectedTask.value?.tags?.some((item) => item.id === tag.id) }
|
||||
function toggleTag(tag: Tag) {
|
||||
if (!selectedTask.value) return
|
||||
@@ -373,6 +414,17 @@ function toggleTag(tag: Tag) {
|
||||
}
|
||||
function toggleFolder(id: string) { const next = new Set(expandedFolders.value); next.has(id) ? next.delete(id) : next.add(id); expandedFolders.value = next }
|
||||
function formatDue(value: string | null) { return value ? new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)) : '' }
|
||||
function formatEventTime(item: CalendarEvent) {
|
||||
if (item.all_day) return '全天'
|
||||
const date = new Date(item.starts_at)
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
async function loadTodayEvents() {
|
||||
try {
|
||||
calendarSubscriptions.value = await api('/calendar-subscriptions')
|
||||
calendarEvents.value = calendarSubscriptions.value.length ? await api(`/calendar-subscriptions/today?day=${dateKey(new Date())}`) : []
|
||||
} catch { calendarEvents.value = [] }
|
||||
}
|
||||
function focusQuick() { nextTick(() => document.querySelector<HTMLInputElement>('.quick-input')?.focus()) }
|
||||
function previousPage() {
|
||||
if (page.value <= 1 || loading.value) return
|
||||
@@ -430,7 +482,7 @@ onMounted(bootstrap)
|
||||
<header class="topbar">
|
||||
<button class="icon mobile-only" aria-label="打开菜单" @click="mobileSidebar=true"><Menu /></button>
|
||||
<div><p>今天也慢慢来</p><h1>{{ activeName }}</h1></div>
|
||||
<label class="search"><Search/><input v-model="query" placeholder="搜索任务…" aria-label="搜索任务"><kbd>⌘ K</kbd></label>
|
||||
<label v-if="['tasks','today','upcoming','trash'].includes(activeView)" class="search"><Search/><input v-model="query" placeholder="搜索任务…" aria-label="搜索任务"><kbd>⌘ K</kbd></label>
|
||||
</header>
|
||||
<template v-if="['habits','settings','calendar'].includes(activeView)">
|
||||
<MvpPanel v-if="['habits','settings'].includes(activeView)" :key="activeView" :view="activeView as 'habits'|'settings'" @changed="refreshAll" @notice="toast" />
|
||||
@@ -438,12 +490,12 @@ onMounted(bootstrap)
|
||||
<header class="view-intro"><div><small>订阅外部日历,每天自动同步</small><h2>日历</h2></div><button class="soft-button" @click="loadCalendarTab"><RefreshCw />刷新</button></header>
|
||||
<div class="settings-grid">
|
||||
<article class="tool-card wide"><Upload /><h3>添加订阅</h3><p>粘贴 ICS / webcal 链接,例如 cal.bboy.app 的合并日历。</p><div class="sub-add"><input v-model="calendarName" placeholder="名称(可选)" aria-label="订阅名称"><input v-model="calendarUrl" placeholder="https://…/all.ics" aria-label="订阅地址"><button class="primary-small" @click="addCalendarSubscription">订阅</button></div><div v-if="calendarSubscriptions.length"><div v-for="sub in calendarSubscriptions" :key="sub.id" class="session-row"><span><b>{{sub.name}}</b><small>{{sub.url}}</small></span><button class="danger-text" @click="removeCalendarSubscription(sub.id)">取消订阅</button></div></div><p v-else class="hint">还没有日历订阅</p></article>
|
||||
<article v-if="calendarSubscriptions.length" class="tool-card wide"><CalendarDays /><h3>今日事件</h3><div v-if="calendarEvents.length" class="calendar-events"><div v-for="ev in calendarEvents" :key="ev.id + ev.starts_at" class="calendar-event"><span class="ev-time">{{ formatEventTime(ev) }}</span><b>{{ ev.title }}</b><small>{{ ev.source_name }}</small></div></div><p v-else class="hint">今天没有日历事件</p></article>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="activeView==='today'">
|
||||
<div class="list-toolbar"><label><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span>共 {{visibleTasks.length}} 项</span></div>
|
||||
<h3 class="section-heading"><ListTodo/>今日任务</h3>
|
||||
</template>
|
||||
<form v-if="activeView!=='trash'" class="quick" @submit.prevent="addTask"><CirclePlus/><input v-model="title" class="quick-input" placeholder="添加任务,按回车保存"><button>添加</button></form>
|
||||
@@ -464,6 +516,10 @@ onMounted(bootstrap)
|
||||
</template>
|
||||
<div v-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{query?'没有匹配的任务':'这里还很安静'}}</b><span>{{query?'换个关键词试试':'写下第一件想完成的小事吧'}}</span></div>
|
||||
</section>
|
||||
<div v-if="activeView==='today' && calendarEvents.length">
|
||||
<h3 class="section-heading"><CalendarDays/>今日日历事件</h3>
|
||||
<div class="calendar-events"><div v-for="ev in calendarEvents" :key="ev.id + ev.starts_at" class="calendar-event"><span class="ev-time">{{ formatEventTime(ev) }}</span><b>{{ ev.title }}</b><small>{{ ev.source_name }}</small></div></div>
|
||||
</div>
|
||||
<div v-if="activeView==='today'">
|
||||
<h3 class="section-heading"><Repeat2/>今日习惯</h3>
|
||||
<MvpPanel view="today-habits" @changed="refreshAll" @notice="toast" />
|
||||
@@ -482,7 +538,7 @@ onMounted(bootstrap)
|
||||
<label v-if="selectedTask.recurrence_rule">重复截止<input v-model="selectedTask.recurrence_end_at" type="date" @change="saveTask"></label>
|
||||
<div class="field"><div class="field-label"><span>标签</span><button class="link" @click="createTag"><Plus/>新建</button></div><div class="tag-picker"><button v-for="tag in tags" :key="tag.id" :class="{chosen:taskHasTag(tag)}" @click="toggleTag(tag);saveTask()"><i :style="{background:tag.color}"/>{{tag.name}}</button><span v-if="!tags.length" class="hint">还没有标签</span></div></div>
|
||||
<div class="field markdown"><div class="field-label"><span>备注</span><span><button :class="{active:!markdownPreview}" @click="markdownPreview=false">编辑</button><button :class="{active:markdownPreview}" @click="markdownPreview=true">预览</button></span></div><div v-if="markdownPreview" class="markdown-preview" v-html="renderMarkdown(selectedTask.description)"/><textarea v-else v-model="selectedTask.description" rows="9" placeholder="支持 Markdown…" @blur="saveTask"/></div>
|
||||
<div class="subtasks"><div class="field-label"><span>子任务</span><button class="link" @click="addSubtask"><Plus/>添加</button></div><button v-for="subtask in tasks.filter(t=>t.parent_id===selectedTask?.id)" :key="subtask.id" class="subtask-detail" @click="toggle(subtask)"><span class="check"><Check v-if="subtask.completed"/></span><span :class="{strike:subtask.completed}">{{subtask.title}}</span></button><span v-if="!tasks.some(t=>t.parent_id===selectedTask?.id)" class="hint">把这件事拆成更小的步骤</span></div>
|
||||
<div class="subtasks"><div class="field-label"><span>子任务</span><button class="link" @click="addSubtask"><Plus/>添加</button></div><button v-for="subtask in selectedTaskSubtasks" :key="subtask.id" class="subtask-detail" @click="toggle(subtask)"><span class="check"><Check v-if="subtask.completed"/></span><span :class="{strike:subtask.completed}">{{subtask.title}}</span></button><span v-if="!selectedTaskSubtasks.length" class="hint">把这件事拆成更小的步骤</span></div>
|
||||
<div class="detail-actions"><button class="secondary" @click="saveTask">保存更改</button><button class="danger-text" @click="removeTask(selectedTask)"><Trash2/>移到回收站</button></div>
|
||||
</div>
|
||||
<div v-else class="paper"><ListChecks/><b>选中一个任务</b><p>日期、优先级、标签、子任务和 Markdown 备注会出现在这里。</p></div>
|
||||
@@ -492,5 +548,12 @@ onMounted(bootstrap)
|
||||
<button v-if="['tasks','today','upcoming'].includes(activeView)" class="fab" aria-label="添加任务" @click="focusQuick"><CirclePlus/></button>
|
||||
<Transition name="toast"><div v-if="notice" class="toast" role="status">{{notice}}</div></Transition>
|
||||
<div v-if="error" class="error-toast" role="alert">{{error}}<button @click="error=''"><X/></button></div>
|
||||
<div v-if="modalVisible" class="modal-mask" @click.self="closeModal">
|
||||
<div class="modal-box" role="dialog" aria-modal="true">
|
||||
<h3>{{ modalTitle }}</h3>
|
||||
<label v-if="modalLabel">{{ modalLabel }}<input v-model="modalValue" class="modal-input" autofocus @keyup.enter="confirmModal"></label>
|
||||
<div class="modal-actions"><button class="secondary" @click="closeModal">取消</button><button class="primary-small" @click="confirmModal">{{ modalConfirmText }}</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,4 +30,4 @@ main{min-width:0;padding:27px 34px 50px;overflow:auto;background:linear-gradient
|
||||
.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.tool-card{display:flex;flex-direction:column;align-items:flex-start;gap:10px}.tool-card>svg{color:var(--accent);width:25px;height:25px}.tool-card p{margin:0;color:var(--muted);font-size:13px}.tool-card.wide{grid-column:1/-1}.file-button input{display:none}.tool-card pre{width:100%;max-height:180px;overflow:auto;background:#f8f3e8;padding:10px;border-radius:8px;font-size:10px}.session-row,.audit-row{width:100%;display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--line);padding:9px 0}.session-row span{display:grid}.session-row small,.audit-row small{color:var(--muted);font-size:11px}.inline-error{padding:9px;border-radius:8px;color:var(--danger);background:#fff0ed}.settings.active{background:var(--accent-soft);color:#b7421e;font-weight:700}
|
||||
@media(max-width:800px){.habit-create{grid-template-columns:1fr 1fr}.habit-create button{justify-content:center}.settings-grid{grid-template-columns:1fr}.tool-card.wide{grid-column:auto}.habit-row{padding:5px 4px 5px 8px}.habit-main{padding:10px 2px}.numeric-action input{width:62px}}
|
||||
@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}
|
||||
.pager{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin:0 0 14px}.pager button:disabled{opacity:.4;cursor:not-allowed}.pager span{color:var(--muted);font-size:13px}
|
||||
.pager{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin:0 0 14px}.pager button:disabled{opacity:.4;cursor:not-allowed}.pager span{color:var(--muted);font-size:13px}.modal-mask{position:fixed;inset:0;z-index:80;background:rgba(45,38,31,.4);display:grid;place-items:center;padding:24px}.modal-box{width:min(360px,92vw);background:#fffdf8;border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);padding:22px;display:grid;gap:14px}.modal-box h3{margin:0;font-size:17px}.modal-box label{display:grid;gap:7px;font-size:12px;font-weight:650;color:#756d61}.modal-input{width:100%;border:1px solid var(--line);background:#fff;padding:11px;border-radius:9px;outline:none;font-size:16px}.modal-actions{display:flex;justify-content:flex-end;gap:8px}.calendar-events{display:grid;gap:8px}.calendar-event{display:grid;grid-template-columns:auto 1fr;align-items:baseline;gap:3px 10px;padding:9px 4px;border-bottom:1px dashed var(--line)}.calendar-event b{font-size:14px}.calendar-event small{grid-column:2;color:var(--muted);font-size:11px}.calendar-event .ev-time{grid-row:1/3;align-self:center;min-width:46px;color:var(--accent);font-weight:800;font-size:12px}
|
||||
|
||||
Reference in New Issue
Block a user