mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-16 13:50:22 +00:00
feat: 支持 v2 插件、独立前端和签名制品发布 (#130)
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
import { createPluginClient } from '@ruoyi/plugin-bridge'
|
||||
import './style.css'
|
||||
|
||||
const element = (id) => document.getElementById(id)
|
||||
const filters = [...element('status-filters').querySelectorAll('button')]
|
||||
const state = {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
status: '',
|
||||
connected: false,
|
||||
ended: false,
|
||||
canWrite: false,
|
||||
loading: true,
|
||||
loaded: false,
|
||||
listError: false,
|
||||
mutating: false,
|
||||
editorLoading: false,
|
||||
editorFailed: false,
|
||||
editorTask: null,
|
||||
deleteTask: null,
|
||||
}
|
||||
let client
|
||||
let context = {}
|
||||
let listController
|
||||
let detailController
|
||||
let listGeneration = 0
|
||||
let detailGeneration = 0
|
||||
|
||||
function showNotice(message, error = false) {
|
||||
element('notice').textContent = message
|
||||
element('notice').classList.toggle('is-error', error)
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
const date = new Date(value)
|
||||
if (!value || Number.isNaN(date.getTime())) return '—'
|
||||
try {
|
||||
return new Intl.DateTimeFormat(context.language || 'zh-CN', {
|
||||
timeZone: context.timeZone || 'UTC',
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
}).format(date)
|
||||
} catch {
|
||||
return date.toISOString().slice(0, 16).replace('T', ' ') + ' UTC'
|
||||
}
|
||||
}
|
||||
|
||||
function setEditorControls() {
|
||||
const writable = state.connected && !state.ended && state.canWrite
|
||||
const blocked = state.editorLoading || state.editorFailed || state.mutating
|
||||
element('task-fields').disabled = blocked
|
||||
element('task-title').readOnly = !writable
|
||||
element('task-description').readOnly = !writable
|
||||
element('task-status').disabled = !writable
|
||||
element('task-priority').disabled = !writable
|
||||
element('form-read-only').hidden = writable || state.editorLoading
|
||||
element('save-task').hidden = !writable
|
||||
element('save-task').disabled = !writable || blocked
|
||||
element('close-dialog').disabled = state.mutating
|
||||
element('cancel-dialog').disabled = state.mutating
|
||||
element('cancel-dialog').textContent = writable ? '取消' : '关闭'
|
||||
element('dialog-heading').textContent = writable
|
||||
? state.editorTask
|
||||
? '编辑任务'
|
||||
: '新增任务'
|
||||
: '查看任务'
|
||||
element('save-task').textContent = state.mutating
|
||||
? '正在保存…'
|
||||
: state.editorTask
|
||||
? '保存修改'
|
||||
: '新增任务'
|
||||
}
|
||||
|
||||
function updateControls() {
|
||||
const available = state.connected && !state.ended
|
||||
const idle = available && !state.loading && !state.mutating
|
||||
element('access-mode').textContent = state.ended
|
||||
? '会话已结束'
|
||||
: !state.connected
|
||||
? '未连接'
|
||||
: state.canWrite
|
||||
? '可编辑 · 共享任务'
|
||||
: '只读 · 共享任务'
|
||||
element('read-only-notice').hidden = !available || state.canWrite
|
||||
element('create-task').disabled = !idle || !state.canWrite
|
||||
element('empty-create').disabled = !idle || !state.canWrite
|
||||
element('empty-create').hidden = !state.canWrite || Boolean(state.status)
|
||||
element('refresh').disabled = !idle
|
||||
element('retry').disabled = !idle
|
||||
element('page-size').disabled = !idle
|
||||
element('previous-page').disabled = !idle || state.page <= 1
|
||||
element('next-page').disabled = !idle || state.page * state.pageSize >= state.total
|
||||
for (const button of filters) {
|
||||
button.disabled = !idle
|
||||
button.setAttribute('aria-pressed', String(button.dataset.status === state.status))
|
||||
}
|
||||
element('list-region').setAttribute('aria-busy', String(state.loading))
|
||||
element('loading-state').hidden = !state.loading
|
||||
element('loading-message').textContent = state.connected ? '正在加载任务…' : '正在连接工作区…'
|
||||
element('task-table').hidden = !state.items.length
|
||||
element('empty-state').hidden =
|
||||
state.loading || state.listError || !state.loaded || Boolean(state.items.length)
|
||||
element('empty-heading').textContent = state.status ? '没有符合条件的任务' : '还没有任务'
|
||||
element('empty-description').textContent = state.status
|
||||
? '试试其他状态,或查看全部任务。'
|
||||
: state.canWrite
|
||||
? '新增第一条任务,开始记录团队待办。'
|
||||
: '当前还没有共享任务,请稍后刷新查看。'
|
||||
element('clear-filter').hidden = !state.status
|
||||
element('list-heading').textContent =
|
||||
state.status === 'todo' ? '待完成任务' : state.status === 'done' ? '已完成任务' : '全部任务'
|
||||
element('task-count').textContent = state.loaded ? `${state.total} 条` : '等待加载'
|
||||
const start = state.total ? (state.page - 1) * state.pageSize + 1 : 0
|
||||
const end = Math.min(state.page * state.pageSize, state.total)
|
||||
element('page-summary').textContent = state.loaded
|
||||
? `显示 ${start}–${end} 条,共 ${state.total} 条任务`
|
||||
: '尚未加载任务'
|
||||
element('page-number').textContent = state.loaded
|
||||
? `${state.page} / ${Math.max(1, Math.ceil(state.total / state.pageSize))}`
|
||||
: '—'
|
||||
for (const button of element('task-rows').querySelectorAll('button')) button.disabled = !idle
|
||||
element('cancel-delete').disabled = state.mutating
|
||||
element('confirm-delete').disabled = !available || !state.canWrite || state.mutating
|
||||
element('confirm-delete').textContent = state.mutating ? '正在删除…' : '确认删除'
|
||||
setEditorControls()
|
||||
}
|
||||
|
||||
function node(tag, className, text) {
|
||||
const result = document.createElement(tag)
|
||||
if (className) result.className = className
|
||||
if (text !== undefined) result.textContent = text
|
||||
return result
|
||||
}
|
||||
|
||||
// 用户内容始终写入文本节点,任务标题和说明不会作为 HTML 执行。
|
||||
function renderRows() {
|
||||
const rows = state.items.map((task) => {
|
||||
const row = node('tr', task.priority === 'high' ? 'is-priority' : '')
|
||||
const titleCell = node('td')
|
||||
const title = node('button', 'task-title-button', task.title)
|
||||
title.type = 'button'
|
||||
title.addEventListener('click', () => openEditor(task.id))
|
||||
titleCell.append(title)
|
||||
if (task.description) titleCell.append(node('p', 'task-description', task.description))
|
||||
titleCell.append(node('span', 'task-id', `#${task.id}`))
|
||||
const status = node('td')
|
||||
status.dataset.label = '状态'
|
||||
status.append(
|
||||
node(
|
||||
'span',
|
||||
`status-badge status-${task.status}`,
|
||||
task.status === 'done' ? '已完成' : '待完成'
|
||||
)
|
||||
)
|
||||
const priority = node('td')
|
||||
priority.dataset.label = '优先级'
|
||||
priority.append(
|
||||
node(
|
||||
'span',
|
||||
task.priority === 'high' ? 'priority-label priority-high' : 'priority-label',
|
||||
task.priority === 'high' ? '优先处理' : '普通'
|
||||
)
|
||||
)
|
||||
const updated = node('td')
|
||||
updated.dataset.label = '更新时间'
|
||||
updated.append(node('span', 'updated-time', formatTime(task.updatedAt)))
|
||||
const actions = node('td')
|
||||
const group = node('div', 'row-actions')
|
||||
if (state.canWrite) {
|
||||
const edit = node('button', 'row-button', '编辑')
|
||||
edit.type = 'button'
|
||||
edit.setAttribute('aria-label', `编辑任务:${task.title}`)
|
||||
edit.addEventListener('click', () => openEditor(task.id))
|
||||
const remove = node('button', 'row-button row-delete', '删除')
|
||||
remove.type = 'button'
|
||||
remove.setAttribute('aria-label', `删除任务:${task.title}`)
|
||||
remove.addEventListener('click', () => openDelete(task))
|
||||
group.append(edit, remove)
|
||||
} else group.append(node('span', 'read-only-label', '仅查看'))
|
||||
actions.append(group)
|
||||
row.append(titleCell, status, priority, updated, actions)
|
||||
return row
|
||||
})
|
||||
element('task-rows').replaceChildren(...rows)
|
||||
}
|
||||
|
||||
async function loadTasks({ reloadInfo = false } = {}) {
|
||||
if (!state.connected || state.ended) return
|
||||
listController?.abort()
|
||||
const controller = new AbortController()
|
||||
listController = controller
|
||||
const generation = ++listGeneration
|
||||
state.loading = true
|
||||
state.listError = false
|
||||
element('list-error').hidden = true
|
||||
updateControls()
|
||||
try {
|
||||
if (reloadInfo) {
|
||||
const info = await client.request(
|
||||
{ method: 'GET', path: 'info' },
|
||||
{ signal: controller.signal }
|
||||
)
|
||||
if (generation !== listGeneration || state.ended) return
|
||||
state.canWrite = info.canWrite === true
|
||||
renderRows()
|
||||
updateControls()
|
||||
}
|
||||
const params = { page: state.page, pageSize: state.pageSize }
|
||||
if (state.status) params.status = state.status
|
||||
const result = await client.request(
|
||||
{ method: 'GET', path: 'tasks', params },
|
||||
{ signal: controller.signal }
|
||||
)
|
||||
if (generation !== listGeneration || state.ended) return
|
||||
state.canWrite = result.canWrite === true
|
||||
state.total = result.total
|
||||
const lastPage = Math.max(1, Math.ceil(result.total / state.pageSize))
|
||||
if (state.page > lastPage) {
|
||||
state.page = lastPage
|
||||
return await loadTasks()
|
||||
}
|
||||
state.items = result.items
|
||||
state.loaded = true
|
||||
renderRows()
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted || generation !== listGeneration || state.ended) return
|
||||
state.listError = true
|
||||
element('list-error-message').textContent = `任务加载失败。${error.message || '请稍后重试。'}`
|
||||
element('list-error').hidden = false
|
||||
} finally {
|
||||
if (generation === listGeneration) {
|
||||
state.loading = false
|
||||
listController = null
|
||||
updateControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
element('title-count').textContent = `${element('task-title').value.length} / 120`
|
||||
element('description-count').textContent = `${element('task-description').value.length} / 500`
|
||||
element('task-title').setCustomValidity('')
|
||||
}
|
||||
|
||||
function fillEditor(task = {}) {
|
||||
element('task-title').value = task.title || ''
|
||||
element('task-description').value = task.description || ''
|
||||
element('task-status').value = task.status || 'todo'
|
||||
element('task-priority').value = task.priority || 'normal'
|
||||
element('task-timestamps').hidden = !task.id
|
||||
element('task-timestamps').textContent = task.id
|
||||
? `创建于 ${formatTime(task.createdAt)} · 更新于 ${formatTime(task.updatedAt)}`
|
||||
: ''
|
||||
updateCounts()
|
||||
}
|
||||
|
||||
async function openEditor(id) {
|
||||
if (!state.connected || state.ended || state.mutating || (!id && !state.canWrite)) return
|
||||
detailController?.abort()
|
||||
const generation = ++detailGeneration
|
||||
state.editorTask = id ? { id } : null
|
||||
state.editorLoading = Boolean(id)
|
||||
state.editorFailed = false
|
||||
element('dialog-kicker').textContent = id ? `共享任务 #${id}` : '新任务'
|
||||
element('form-error').hidden = true
|
||||
element('save-state').textContent = ''
|
||||
element('detail-loading').hidden = !id
|
||||
fillEditor()
|
||||
setEditorControls()
|
||||
element('task-dialog').showModal()
|
||||
if (!id) {
|
||||
element('task-title').focus()
|
||||
return
|
||||
}
|
||||
const controller = new AbortController()
|
||||
detailController = controller
|
||||
try {
|
||||
const task = await client.request(
|
||||
{ method: 'GET', path: `tasks/${id}` },
|
||||
{ signal: controller.signal }
|
||||
)
|
||||
if (controller.signal.aborted || generation !== detailGeneration || state.ended) return
|
||||
state.editorTask = task
|
||||
fillEditor(task)
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted || generation !== detailGeneration || state.ended) return
|
||||
state.editorFailed = true
|
||||
element('form-error').textContent = `任务详情加载失败。${error.message || '请关闭后重试。'}`
|
||||
element('form-error').hidden = false
|
||||
element('form-error').focus()
|
||||
} finally {
|
||||
if (generation === detailGeneration) {
|
||||
state.editorLoading = false
|
||||
element('detail-loading').hidden = true
|
||||
setEditorControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
if (state.mutating) return
|
||||
detailController?.abort()
|
||||
detailGeneration++
|
||||
element('task-dialog').close()
|
||||
}
|
||||
|
||||
async function checkWriteAccess() {
|
||||
try {
|
||||
const info = await client.request({ method: 'GET', path: 'info' })
|
||||
if (!state.ended) {
|
||||
state.canWrite = info.canWrite === true
|
||||
renderRows()
|
||||
}
|
||||
} catch {
|
||||
/* 保存原始错误;后续刷新仍可重新读取权限。 */
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTask(event) {
|
||||
event.preventDefault()
|
||||
if (state.mutating || state.editorLoading || state.editorFailed || !state.canWrite || state.ended)
|
||||
return
|
||||
const title = element('task-title').value.trim()
|
||||
if (!title) element('task-title').setCustomValidity('请输入任务标题,不能只包含空格。')
|
||||
if (!element('task-form').reportValidity()) return
|
||||
const data = {
|
||||
title,
|
||||
description: element('task-description').value,
|
||||
status: element('task-status').value,
|
||||
priority: element('task-priority').value,
|
||||
}
|
||||
const id = state.editorTask?.id
|
||||
state.mutating = true
|
||||
element('form-error').hidden = true
|
||||
element('save-state').textContent = '正在保存,请稍候…'
|
||||
updateControls()
|
||||
let saved = false
|
||||
try {
|
||||
await client.request({
|
||||
method: id ? 'PUT' : 'POST',
|
||||
path: id ? `tasks/${id}` : 'tasks',
|
||||
data,
|
||||
})
|
||||
if (state.ended) return
|
||||
saved = true
|
||||
element('task-dialog').close()
|
||||
const filteredOut = state.status && state.status !== data.status
|
||||
showNotice(
|
||||
`${id ? '任务已更新。' : '任务已新增。'}${filteredOut ? '该任务不在当前状态筛选中,可切换到“全部”查看。' : ''}`
|
||||
)
|
||||
if (!id) state.page = 1
|
||||
} catch (error) {
|
||||
if (state.ended) return
|
||||
element('form-error').textContent =
|
||||
`保存失败,填写内容已保留。${error.message || '请稍后重试。'}`
|
||||
element('form-error').hidden = false
|
||||
element('form-error').focus()
|
||||
await checkWriteAccess()
|
||||
} finally {
|
||||
state.mutating = false
|
||||
element('save-state').textContent = ''
|
||||
updateControls()
|
||||
}
|
||||
if (saved) await loadTasks()
|
||||
}
|
||||
|
||||
function openDelete(task) {
|
||||
if (!state.canWrite || state.mutating || state.ended) return
|
||||
state.deleteTask = task
|
||||
element('delete-title').textContent = task.title
|
||||
element('delete-error').hidden = true
|
||||
element('delete-dialog').showModal()
|
||||
element('cancel-delete').focus()
|
||||
}
|
||||
|
||||
async function deleteTask() {
|
||||
if (!state.deleteTask || !state.canWrite || state.mutating || state.ended) return
|
||||
state.mutating = true
|
||||
element('delete-error').hidden = true
|
||||
updateControls()
|
||||
let deleted = false
|
||||
try {
|
||||
await client.request({
|
||||
method: 'DELETE',
|
||||
path: `tasks/${state.deleteTask.id}`,
|
||||
})
|
||||
if (state.ended) return
|
||||
deleted = true
|
||||
element('delete-dialog').close()
|
||||
showNotice('任务已删除。')
|
||||
} catch (error) {
|
||||
if (state.ended) return
|
||||
element('delete-error').textContent = `删除失败。${error.message || '请稍后重试。'}`
|
||||
element('delete-error').hidden = false
|
||||
element('delete-error').focus()
|
||||
await checkWriteAccess()
|
||||
} finally {
|
||||
state.mutating = false
|
||||
updateControls()
|
||||
}
|
||||
if (deleted) await loadTasks()
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
state.status = status
|
||||
state.page = 1
|
||||
reloadQuery()
|
||||
}
|
||||
|
||||
function reloadQuery() {
|
||||
// 切换查询条件后清除旧结果,避免把上一种筛选误认为当前列表。
|
||||
state.items = []
|
||||
state.loaded = false
|
||||
renderRows()
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
function applyContext(next) {
|
||||
context = { ...context, ...next }
|
||||
document.documentElement.dataset.theme = context.theme?.mode === 'dark' ? 'dark' : 'light'
|
||||
document.documentElement.lang = context.language || 'zh-CN'
|
||||
renderRows()
|
||||
updateControls()
|
||||
}
|
||||
|
||||
function endSession() {
|
||||
state.ended = true
|
||||
state.canWrite = false
|
||||
state.loading = false
|
||||
listController?.abort()
|
||||
detailController?.abort()
|
||||
listGeneration++
|
||||
detailGeneration++
|
||||
element('task-dialog').close()
|
||||
element('delete-dialog').close()
|
||||
showNotice('会话已结束,请重新登录管理平台后打开任务清单。', true)
|
||||
renderRows()
|
||||
updateControls()
|
||||
}
|
||||
|
||||
for (const button of filters)
|
||||
button.addEventListener('click', () => setStatus(button.dataset.status))
|
||||
element('clear-filter').addEventListener('click', () => setStatus(''))
|
||||
for (const id of ['create-task', 'empty-create'])
|
||||
element(id).addEventListener('click', () => openEditor())
|
||||
for (const id of ['refresh', 'retry'])
|
||||
element(id).addEventListener('click', () => loadTasks({ reloadInfo: true }))
|
||||
element('previous-page').addEventListener('click', () => {
|
||||
state.page--
|
||||
reloadQuery()
|
||||
})
|
||||
element('next-page').addEventListener('click', () => {
|
||||
state.page++
|
||||
reloadQuery()
|
||||
})
|
||||
element('page-size').addEventListener('change', () => {
|
||||
state.pageSize = Number(element('page-size').value)
|
||||
state.page = 1
|
||||
reloadQuery()
|
||||
})
|
||||
for (const id of ['close-dialog', 'cancel-dialog'])
|
||||
element(id).addEventListener('click', closeEditor)
|
||||
element('task-dialog').addEventListener('cancel', (event) => {
|
||||
event.preventDefault()
|
||||
closeEditor()
|
||||
})
|
||||
element('delete-dialog').addEventListener('cancel', (event) => {
|
||||
if (state.mutating) event.preventDefault()
|
||||
})
|
||||
element('cancel-delete').addEventListener('click', () => {
|
||||
if (!state.mutating) element('delete-dialog').close()
|
||||
})
|
||||
element('confirm-delete').addEventListener('click', deleteTask)
|
||||
element('task-form').addEventListener('submit', saveTask)
|
||||
for (const id of ['task-title', 'task-description'])
|
||||
element(id).addEventListener('input', updateCounts)
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
const config = JSON.parse(element('ruoyi-plugin-config')?.textContent || '{}')
|
||||
if (config.pluginId !== 'task_demo') throw new Error('请从管理平台或本地模拟宿主打开任务清单。')
|
||||
client = createPluginClient({ pluginId: config.pluginId })
|
||||
client.subscribe(({ type, payload }) => {
|
||||
if (type === 'preferences') applyContext(payload)
|
||||
if (type === 'refresh') loadTasks({ reloadInfo: true })
|
||||
if (type === 'logout') endSession()
|
||||
})
|
||||
applyContext(await client.ready)
|
||||
state.connected = true
|
||||
await loadTasks({ reloadInfo: true })
|
||||
} catch (error) {
|
||||
state.loading = false
|
||||
showNotice(error.message || '连接失败,请重新打开任务清单。', true)
|
||||
updateControls()
|
||||
}
|
||||
}
|
||||
connect()
|
||||
window.addEventListener(
|
||||
'pagehide',
|
||||
() => {
|
||||
listController?.abort()
|
||||
detailController?.abort()
|
||||
client?.destroy()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
@@ -0,0 +1,952 @@
|
||||
:root {
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
||||
color: var(--ink);
|
||||
background: var(--canvas);
|
||||
color-scheme: light;
|
||||
--canvas: #f3f6f7;
|
||||
--surface: #ffffff;
|
||||
--surface-subtle: #f7f9fa;
|
||||
--ink: #172b36;
|
||||
--muted: #516574;
|
||||
--border: #d6e0e4;
|
||||
--control-border: #879aa4;
|
||||
--accent: #176b62;
|
||||
--accent-hover: #10564e;
|
||||
--on-accent: #ffffff;
|
||||
--accent-soft: #e6f3f0;
|
||||
--pending: #805300;
|
||||
--pending-soft: #fff3d0;
|
||||
--complete: #136849;
|
||||
--complete-soft: #e0f3e9;
|
||||
--danger: #b42318;
|
||||
--danger-hover: #8f1d14;
|
||||
--danger-soft: #fff0ee;
|
||||
--on-danger: #ffffff;
|
||||
--scrim: rgb(10 25 35 / 48%);
|
||||
--shadow: 0 18px 60px rgb(18 41 53 / 18%);
|
||||
--mono: 'Cascadia Mono', Consolas, monospace;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
--canvas: #111c25;
|
||||
--surface: #172631;
|
||||
--surface-subtle: #1c2e3a;
|
||||
--ink: #e8f0f5;
|
||||
--muted: #aec0c9;
|
||||
--border: #344854;
|
||||
--control-border: #6c8390;
|
||||
--accent: #79d8c6;
|
||||
--accent-hover: #a5e7db;
|
||||
--on-accent: #0d3028;
|
||||
--accent-soft: #193c38;
|
||||
--pending: #f4cd79;
|
||||
--pending-soft: #43391e;
|
||||
--complete: #8ce0b8;
|
||||
--complete-soft: #1e3d32;
|
||||
--danger: #ffad9f;
|
||||
--danger-hover: #ffccbf;
|
||||
--danger-soft: #432a29;
|
||||
--on-danger: #431b15;
|
||||
--scrim: rgb(3 10 15 / 68%);
|
||||
--shadow: 0 18px 60px rgb(0 0 0 / 35%);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
button,
|
||||
select {
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
button {
|
||||
color: inherit;
|
||||
}
|
||||
button:disabled,
|
||||
select:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
border-color 160ms ease,
|
||||
color 160ms ease;
|
||||
}
|
||||
:focus-visible {
|
||||
outline: 3px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
z-index: 5;
|
||||
padding: 10px 16px;
|
||||
transform: translateY(-180%);
|
||||
color: var(--on-accent);
|
||||
background: var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.workspace {
|
||||
width: min(100%, 1280px);
|
||||
margin: 0 auto;
|
||||
padding: 32px clamp(16px, 3vw, 40px) 40px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.page-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.page-mark svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.eyebrow {
|
||||
margin-bottom: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Microsoft YaHei UI', 'PingFang SC', sans-serif;
|
||||
font-size: 28px;
|
||||
line-height: 1.35;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.page-description {
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.access-mode {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
min-height: 42px;
|
||||
padding: 9px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.button-primary {
|
||||
color: var(--on-accent);
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.button-primary:not(:disabled):hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
.button-secondary {
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
border-color: var(--control-border);
|
||||
}
|
||||
.button-secondary:not(:disabled):hover {
|
||||
background: var(--surface-subtle);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.button-danger {
|
||||
background: var(--danger);
|
||||
color: var(--on-danger);
|
||||
}
|
||||
.button-danger:not(:disabled):hover {
|
||||
background: var(--danger-hover);
|
||||
}
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.read-only-notice {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
color: var(--pending);
|
||||
background: var(--pending-soft);
|
||||
border-radius: 7px;
|
||||
}
|
||||
.notice {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 7px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.notice:empty {
|
||||
display: none;
|
||||
}
|
||||
.notice.is-error {
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
.list-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 22px 24px 16px;
|
||||
}
|
||||
.list-heading-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.list-heading-row h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.task-count {
|
||||
font-family: var(--mono);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
padding: 2px 7px;
|
||||
background: var(--surface-subtle);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 24px 16px;
|
||||
}
|
||||
.status-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
.filter-button {
|
||||
min-height: 36px;
|
||||
padding: 6px 17px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.filter-button[aria-pressed='true'] {
|
||||
color: var(--accent);
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
font-weight: 650;
|
||||
}
|
||||
.filter-button:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
.list-hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.list-region {
|
||||
min-height: 240px;
|
||||
position: relative;
|
||||
}
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 30px 20px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.loading-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: loading-pulse 1s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes loading-pulse {
|
||||
from {
|
||||
opacity: 0.3;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.list-region[aria-busy='true'] .task-table {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.task-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
.task-table th {
|
||||
padding: 12px 16px;
|
||||
background: var(--surface-subtle);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.task-table th:first-child {
|
||||
width: 43%;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.task-table th:nth-child(2) {
|
||||
width: 12%;
|
||||
}
|
||||
.task-table th:nth-child(3) {
|
||||
width: 12%;
|
||||
}
|
||||
.task-table th:nth-child(4) {
|
||||
width: 19%;
|
||||
}
|
||||
.task-table th:nth-child(5) {
|
||||
width: 14%;
|
||||
}
|
||||
.task-table td {
|
||||
padding: 18px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.task-table td:first-child {
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
}
|
||||
.task-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.task-table tbody tr:hover {
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
.task-table .is-priority td:first-child::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 18px;
|
||||
bottom: 18px;
|
||||
width: 3px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
background: var(--pending);
|
||||
}
|
||||
.task-title-button {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.task-title-button:not(:disabled):hover {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.task-description {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.task-id {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font: 11px/1.5 var(--mono);
|
||||
color: var(--muted);
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-badge::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.status-todo {
|
||||
color: var(--pending);
|
||||
background: var(--pending-soft);
|
||||
}
|
||||
.status-done {
|
||||
color: var(--complete);
|
||||
background: var(--complete-soft);
|
||||
}
|
||||
.priority-label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.priority-high {
|
||||
color: var(--pending);
|
||||
font-weight: 600;
|
||||
}
|
||||
.updated-time {
|
||||
color: var(--muted);
|
||||
font: 12px/1.7 var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.row-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.row-button {
|
||||
min-height: 32px;
|
||||
padding: 3px 0;
|
||||
color: var(--accent);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.row-button:not(:disabled):hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.row-delete {
|
||||
color: var(--danger);
|
||||
}
|
||||
.actions-heading {
|
||||
text-align: right;
|
||||
}
|
||||
.read-only-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.empty-state {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
padding: 42px 24px 52px;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.empty-state h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
.empty-state p {
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.error-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: 0 24px 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: 7px;
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.pagination p {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.page-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
.page-size-label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.page-controls select {
|
||||
width: 79px;
|
||||
min-height: 38px;
|
||||
}
|
||||
.page-controls .button {
|
||||
min-height: 38px;
|
||||
padding: 7px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.page-number {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font: 12px/1.5 var(--mono);
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--muted);
|
||||
opacity: 0.85;
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
input:read-only,
|
||||
textarea:read-only {
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
.dialog {
|
||||
width: min(600px, calc(100% - 32px));
|
||||
max-height: calc(100dvh - 32px);
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: auto;
|
||||
}
|
||||
.dialog::backdrop {
|
||||
background: var(--scrim);
|
||||
}
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 24px 24px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.dialog h2 {
|
||||
font-size: 21px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
min-height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
}
|
||||
.icon-button:not(:disabled):hover {
|
||||
color: var(--ink);
|
||||
background: var(--surface-subtle);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.dialog-body {
|
||||
padding: 22px 24px 24px;
|
||||
}
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.field > label,
|
||||
.field-label label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.field-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
}
|
||||
.field-count {
|
||||
color: var(--muted);
|
||||
font: 11px/1.5 var(--mono);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.field-hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.required-mark {
|
||||
color: var(--danger);
|
||||
}
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
.field-grid .field {
|
||||
margin: 0;
|
||||
}
|
||||
.form-error {
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 18px;
|
||||
color: var(--danger);
|
||||
border: 1px solid var(--danger);
|
||||
background: var(--danger-soft);
|
||||
border-radius: 6px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.inline-notice {
|
||||
padding: 10px 12px;
|
||||
color: var(--muted);
|
||||
background: var(--surface-subtle);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.task-timestamps {
|
||||
margin-top: 20px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 18px 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.save-state {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.delete-dialog {
|
||||
width: min(460px, calc(100% - 32px));
|
||||
}
|
||||
.delete-task-title {
|
||||
padding: 12px 14px;
|
||||
margin: 18px 0 12px;
|
||||
background: var(--surface-subtle);
|
||||
border-left: 3px solid var(--danger);
|
||||
border-radius: 0 6px 6px 0;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.muted-text {
|
||||
color: var(--muted);
|
||||
}
|
||||
.delete-dialog .form-error {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.task-table th:first-child {
|
||||
width: 37%;
|
||||
}
|
||||
.task-table th:nth-child(2),
|
||||
.task-table th:nth-child(3) {
|
||||
width: 13%;
|
||||
}
|
||||
.task-table th:nth-child(4) {
|
||||
width: 22%;
|
||||
}
|
||||
.task-table th:nth-child(5) {
|
||||
width: 15%;
|
||||
}
|
||||
.task-table th,
|
||||
.task-table td {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.pagination {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.page-controls {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
font-size: 15px;
|
||||
}
|
||||
.workspace {
|
||||
padding-top: 22px;
|
||||
}
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-heading {
|
||||
gap: 12px;
|
||||
}
|
||||
.page-mark {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 25px;
|
||||
}
|
||||
.page-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
.header-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.button,
|
||||
.filter-button,
|
||||
.page-controls .button,
|
||||
.page-controls select,
|
||||
.row-button {
|
||||
min-height: 44px;
|
||||
}
|
||||
.list-toolbar {
|
||||
padding: 18px 16px 12px;
|
||||
}
|
||||
.list-heading-row {
|
||||
gap: 6px;
|
||||
}
|
||||
.list-heading-row h2 {
|
||||
font-size: 17px;
|
||||
}
|
||||
.list-toolbar .button {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.filter-row {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
.status-filters {
|
||||
width: 100%;
|
||||
}
|
||||
.filter-button {
|
||||
flex: 1;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.list-hint {
|
||||
display: none;
|
||||
}
|
||||
.task-table,
|
||||
.task-table tbody {
|
||||
display: block;
|
||||
}
|
||||
.task-table thead {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
.task-table tr {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px 14px;
|
||||
padding: 18px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.task-table td {
|
||||
display: block;
|
||||
padding: 0 !important;
|
||||
border: 0;
|
||||
}
|
||||
.task-table td:first-child,
|
||||
.task-table td:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.task-table td:nth-child(4) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.task-table td[data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.task-table .is-priority td:first-child::before {
|
||||
left: -16px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.task-title-button {
|
||||
font-size: 16px;
|
||||
min-height: 32px;
|
||||
}
|
||||
.row-actions {
|
||||
gap: 20px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.row-button {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
.pagination {
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-controls {
|
||||
gap: 6px;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page-size-label {
|
||||
display: none;
|
||||
}
|
||||
.page-controls select {
|
||||
width: 72px;
|
||||
padding: 8px;
|
||||
}
|
||||
.page-controls .button {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.page-number {
|
||||
min-width: 28px;
|
||||
}
|
||||
.error-panel {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dialog-header {
|
||||
padding: 20px 18px 16px;
|
||||
}
|
||||
.dialog-body {
|
||||
padding: 20px 18px;
|
||||
}
|
||||
.dialog-footer {
|
||||
padding: 16px 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.field-grid {
|
||||
gap: 12px;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user