Refine scheduled task dashboard UX (#135)
* add gated codex goals support * sync README SDK versions * bump claude agent sdk * add codex goals settings toggle * reuse status dashboard message and simplify settings actions * refine settings page UX * fix settings nav hash routing * add dashboard UX verification * refine dashboard settings and inbox UX * remove inbox top-level navigation * remove dashboard health top-level navigation * fix: allow runtime image attachment paths * feat: configure attachment allowlist dirs * fix: clean duplicate dashboard status messages * fix: poll dashboard duplicate cleanup between status updates * fix: delete dashboard duplicates on create * Refine settings IA with tabbed sections * Add read-only runtime inventory settings * Fix runtime inventory MCP JSON parsing * Add room skill settings inventory * Add room skill setting mutations * Apply room skill overrides to runner spawn * Refine scheduled task dashboard UX
This commit is contained in:
688
apps/dashboard/src/TaskPanel.css
Normal file
688
apps/dashboard/src/TaskPanel.css
Normal file
@@ -0,0 +1,688 @@
|
||||
.task-board {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.task-command-center {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(210px, 0.75fr) minmax(
|
||||
260px,
|
||||
1fr
|
||||
);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 26px;
|
||||
background:
|
||||
radial-gradient(circle at 12% 0%, rgba(191, 95, 44, 0.18), transparent 34%),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.065),
|
||||
rgba(255, 255, 255, 0.025)
|
||||
);
|
||||
box-shadow: 0 18px 48px rgba(16, 10, 6, 0.08);
|
||||
}
|
||||
|
||||
.task-command-copy,
|
||||
.task-command-next,
|
||||
.task-summary-metric {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-command-copy strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: clamp(23px, 4vw, 34px);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.task-command-copy p {
|
||||
max-width: 520px;
|
||||
margin: 9px 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-command-next {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
align-content: center;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(191, 95, 44, 0.22);
|
||||
border-radius: 18px;
|
||||
background: rgba(191, 95, 44, 0.08);
|
||||
}
|
||||
|
||||
.task-command-next small,
|
||||
.task-command-next span,
|
||||
.task-summary-metric small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.task-command-next strong {
|
||||
overflow: hidden;
|
||||
font-size: 21px;
|
||||
letter-spacing: -0.04em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-command-next span {
|
||||
overflow: hidden;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-command-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-summary-metric {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
align-content: center;
|
||||
min-height: 70px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.task-summary-metric strong {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.task-create-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 0.42fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.task-edit-form {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(0, 1.4fr) minmax(110px, 0.6fr) minmax(0, 1fr)
|
||||
auto;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
margin-top: 8px;
|
||||
padding: 9px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.task-create-head {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(191, 95, 44, 0.2);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(191, 95, 44, 0.14), transparent),
|
||||
rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.task-create-head strong {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 22px;
|
||||
line-height: 1.06;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.task-create-head p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.task-create-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-form-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1.2fr) repeat(3, minmax(110px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-form-footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-form-footer > span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.task-create-form label,
|
||||
.task-edit-form label {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-create-form input,
|
||||
.task-create-form select,
|
||||
.task-create-form textarea,
|
||||
.task-edit-form input,
|
||||
.task-edit-form select,
|
||||
.task-edit-form textarea {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--panel-border-strong);
|
||||
border-radius: 15px;
|
||||
padding: 10px 12px;
|
||||
color: var(--ink);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-create-form textarea,
|
||||
.task-edit-form textarea {
|
||||
min-height: 82px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.task-edit-form textarea {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.task-create-form input:focus-visible,
|
||||
.task-create-form select:focus-visible,
|
||||
.task-create-form textarea:focus-visible,
|
||||
.task-edit-form input:focus-visible,
|
||||
.task-edit-form select:focus-visible,
|
||||
.task-edit-form textarea:focus-visible {
|
||||
outline: 3px solid rgba(191, 95, 44, 0.18);
|
||||
}
|
||||
|
||||
.task-create-form button,
|
||||
.task-edit-form button {
|
||||
min-height: 44px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 0 18px;
|
||||
color: var(--bg);
|
||||
background: var(--accent);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-create-form button:disabled,
|
||||
.task-edit-form button:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.task-form-wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.task-create-form .task-form-wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.task-lanes {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent),
|
||||
rgba(255, 255, 255, 0.018);
|
||||
}
|
||||
|
||||
.task-group-completed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.task-group-completed[open] {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.task-group-completed summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-group-completed summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-group-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-group-head strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(255, 255, 255, 0.055), transparent 58%),
|
||||
var(--bg-card);
|
||||
box-shadow: 0 14px 34px rgba(15, 10, 7, 0.05);
|
||||
}
|
||||
|
||||
.task-card-main,
|
||||
.task-time-grid,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-kind {
|
||||
color: var(--accent-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.task-title strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.02em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-meta-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-meta-strip span {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
min-height: 26px;
|
||||
align-items: center;
|
||||
padding: 0 9px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-status-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
min-height: 44px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--panel-border-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-action:hover,
|
||||
.task-action:focus-visible {
|
||||
border-color: rgba(191, 95, 44, 0.42);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(191, 95, 44, 0.12);
|
||||
}
|
||||
|
||||
.task-action:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.task-action.is-busy {
|
||||
display: inline-flex;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: progress;
|
||||
opacity: 0.85;
|
||||
background: rgba(191, 95, 44, 0.12);
|
||||
border-color: rgba(191, 95, 44, 0.32);
|
||||
}
|
||||
|
||||
.task-action.is-busy::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: ej-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ej-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.task-action-cancel {
|
||||
color: #8f2f22;
|
||||
border-color: rgba(183, 71, 52, 0.22);
|
||||
background: rgba(183, 71, 52, 0.08);
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(150px, 1.2fr) repeat(2, minmax(0, 0.9fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-time-grid > span,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
padding: 9px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.task-next-run {
|
||||
border-color: rgba(191, 95, 44, 0.25) !important;
|
||||
background: rgba(191, 95, 44, 0.08) !important;
|
||||
}
|
||||
|
||||
.task-time-grid small,
|
||||
.task-suspended span,
|
||||
.task-result span,
|
||||
.task-time-grid em,
|
||||
.task-suspended em {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-time-grid small,
|
||||
.task-time-grid strong,
|
||||
.task-time-grid em,
|
||||
.task-suspended span,
|
||||
.task-suspended strong,
|
||||
.task-suspended em,
|
||||
.task-result span,
|
||||
.task-result strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-time-grid strong,
|
||||
.task-suspended strong,
|
||||
.task-result strong {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.task-next-run strong {
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.task-suspended {
|
||||
border-color: rgba(181, 138, 34, 0.22);
|
||||
background: rgba(181, 138, 34, 0.08);
|
||||
}
|
||||
|
||||
.task-result {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border-color: rgba(63, 127, 81, 0.15);
|
||||
}
|
||||
|
||||
.result-fail {
|
||||
border-color: rgba(183, 71, 52, 0.28);
|
||||
background: rgba(183, 71, 52, 0.08);
|
||||
}
|
||||
|
||||
.result-none {
|
||||
border-color: rgba(109, 115, 95, 0.14);
|
||||
}
|
||||
|
||||
.task-prompt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.task-edit summary,
|
||||
.task-prompt summary {
|
||||
min-height: 32px;
|
||||
color: var(--accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-prompt p {
|
||||
margin: 4px 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-prompt small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-family:
|
||||
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-group-empty {
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.pill-watchers {
|
||||
color: #173e23;
|
||||
background: rgba(63, 127, 81, 0.18);
|
||||
}
|
||||
|
||||
.pill-scheduled {
|
||||
color: #6e4a05;
|
||||
background: rgba(181, 138, 34, 0.16);
|
||||
}
|
||||
|
||||
.pill-paused {
|
||||
color: #6e4a05;
|
||||
background: rgba(181, 138, 34, 0.2);
|
||||
}
|
||||
|
||||
.pill-completed {
|
||||
color: #625d52;
|
||||
background: rgba(109, 115, 95, 0.16);
|
||||
}
|
||||
|
||||
.task-card > p {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.task-command-center,
|
||||
.task-create-form,
|
||||
.task-form-controls,
|
||||
.task-card-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-command-center {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.task-command-metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.task-summary-metric {
|
||||
min-height: 62px;
|
||||
}
|
||||
|
||||
.task-create-head {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.task-create-head strong {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.task-form-footer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.task-form-footer button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task-edit-form {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 7px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task-form-wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
gap: 7px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
min-height: 44px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.task-card-main {
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-time-grid > span,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.task-prompt summary {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.task-command-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,9 @@ describe('TaskPanel', () => {
|
||||
|
||||
expect(html).toContain(t.tasks.groups.scheduled);
|
||||
expect(html).toContain('eyejokerdb-main');
|
||||
expect(html).toContain(t.panels.scheduled);
|
||||
expect(html).toContain(t.tasks.next);
|
||||
expect(html).toContain('10m');
|
||||
expect(html).toContain(t.tasks.actions.pause);
|
||||
expect(html).toContain(t.tasks.actions.cancel);
|
||||
expect(html).toContain('Task completed');
|
||||
|
||||
@@ -13,6 +13,7 @@ import { localeTags, type Locale, type Messages } from './i18n';
|
||||
import { redactSecretsForPreview } from './redaction';
|
||||
import { statusLabel } from './dashboardHelpers';
|
||||
import { TaskActionButtons, type TaskActionKey } from './TaskActionButtons';
|
||||
import './TaskPanel.css';
|
||||
|
||||
export type { TaskActionKey } from './TaskActionButtons';
|
||||
|
||||
@@ -41,6 +42,15 @@ interface TaskGroup {
|
||||
tasks: DashboardTask[];
|
||||
}
|
||||
|
||||
interface TaskSummary {
|
||||
completed: number;
|
||||
nextTask: DashboardTask | null;
|
||||
paused: number;
|
||||
scheduled: number;
|
||||
total: number;
|
||||
watchers: number;
|
||||
}
|
||||
|
||||
interface TaskCreateFormProps {
|
||||
rooms: RoomOption[];
|
||||
onTaskCreate: (input: CreateScheduledTaskInput) => void;
|
||||
@@ -170,6 +180,23 @@ function taskDisplayName(task: DashboardTask, t: Messages): string {
|
||||
return task.id;
|
||||
}
|
||||
|
||||
function taskHeadline(task: DashboardTask, t: Messages): string {
|
||||
if (task.isWatcher && task.ciProvider) {
|
||||
return `${t.tasks.ciWatch} · ${task.ciProvider}`;
|
||||
}
|
||||
return safePreview(task.promptPreview, taskDisplayName(task, t));
|
||||
}
|
||||
|
||||
function taskScheduleText(task: DashboardTask, t: Messages): string {
|
||||
const scheduleType =
|
||||
task.scheduleType === 'cron' ||
|
||||
task.scheduleType === 'interval' ||
|
||||
task.scheduleType === 'once'
|
||||
? t.tasks.scheduleTypes[task.scheduleType]
|
||||
: task.scheduleType;
|
||||
return `${scheduleType} · ${task.scheduleValue || '-'}`;
|
||||
}
|
||||
|
||||
function isTaskScheduleType(
|
||||
value: FormDataEntryValue | null,
|
||||
): value is DashboardTaskScheduleType {
|
||||
@@ -259,6 +286,102 @@ function buildTaskGroups(tasks: DashboardTask[]): TaskGroup[] {
|
||||
];
|
||||
}
|
||||
|
||||
function buildTaskSummary(tasks: DashboardTask[]): TaskSummary {
|
||||
const summary: TaskSummary = {
|
||||
completed: 0,
|
||||
nextTask: null,
|
||||
paused: 0,
|
||||
scheduled: 0,
|
||||
total: tasks.length,
|
||||
watchers: 0,
|
||||
};
|
||||
|
||||
for (const task of tasks) {
|
||||
const group = taskGroupKey(task);
|
||||
if (group === 'watchers') summary.watchers += 1;
|
||||
if (group === 'scheduled') summary.scheduled += 1;
|
||||
if (group === 'paused') summary.paused += 1;
|
||||
if (group === 'completed') summary.completed += 1;
|
||||
|
||||
if (task.status !== 'active' || !task.nextRun) continue;
|
||||
const nextTime = new Date(task.nextRun).getTime();
|
||||
if (Number.isNaN(nextTime)) continue;
|
||||
const currentTime = summary.nextTask?.nextRun
|
||||
? new Date(summary.nextTask.nextRun).getTime()
|
||||
: Number.POSITIVE_INFINITY;
|
||||
if (nextTime < currentTime) {
|
||||
summary.nextTask = task;
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function TaskSummaryMetric({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<span className="task-summary-metric">
|
||||
<strong>{value}</strong>
|
||||
<small>{label}</small>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskBoardSummary({
|
||||
locale,
|
||||
summary,
|
||||
t,
|
||||
}: {
|
||||
locale: Locale;
|
||||
summary: TaskSummary;
|
||||
t: Messages;
|
||||
}) {
|
||||
const nextTask = summary.nextTask;
|
||||
return (
|
||||
<section className="task-command-center" aria-label={t.panels.scheduled}>
|
||||
<div className="task-command-copy">
|
||||
<span className="eyebrow">{t.panels.scheduled}</span>
|
||||
<strong>
|
||||
{summary.total} {t.tasks.count}
|
||||
</strong>
|
||||
<p>
|
||||
{nextTask
|
||||
? `${t.tasks.next}: ${taskHeadline(nextTask, t)}`
|
||||
: t.tasks.empty}
|
||||
</p>
|
||||
</div>
|
||||
<div className="task-command-next">
|
||||
<small>{t.tasks.next}</small>
|
||||
<strong>
|
||||
{nextTask ? formatRelativeDate(nextTask.nextRun, locale, t) : '-'}
|
||||
</strong>
|
||||
<span>
|
||||
{nextTask
|
||||
? `${nextTask.groupFolder} · ${taskScheduleText(nextTask, t)}`
|
||||
: t.tasks.noTime}
|
||||
</span>
|
||||
</div>
|
||||
<div className="task-command-metrics">
|
||||
<TaskSummaryMetric
|
||||
label={t.tasks.groups.scheduled}
|
||||
value={summary.scheduled}
|
||||
/>
|
||||
<TaskSummaryMetric
|
||||
label={t.tasks.groups.watchers}
|
||||
value={summary.watchers}
|
||||
/>
|
||||
<TaskSummaryMetric
|
||||
label={t.tasks.groups.paused}
|
||||
value={summary.paused}
|
||||
/>
|
||||
<TaskSummaryMetric
|
||||
label={t.tasks.groups.completed}
|
||||
value={summary.completed}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskCreateForm({
|
||||
rooms,
|
||||
onTaskCreate,
|
||||
@@ -280,54 +403,62 @@ function TaskCreateForm({
|
||||
<div className="task-create-head">
|
||||
<span className="eyebrow">{t.tasks.createTitle}</span>
|
||||
<strong>{t.tasks.createSubtitle}</strong>
|
||||
<p>{t.tasks.scheduleValueHint}</p>
|
||||
</div>
|
||||
<div className="task-create-body">
|
||||
<label className="task-form-wide">
|
||||
<span>{t.tasks.prompt}</span>
|
||||
<textarea
|
||||
name="prompt"
|
||||
placeholder={t.tasks.promptPlaceholder}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<div className="task-form-controls">
|
||||
<label>
|
||||
<span>{t.tasks.room}</span>
|
||||
<select name="roomJid" required>
|
||||
<option value="">{t.tasks.selectRoom}</option>
|
||||
{rooms.map((room) => (
|
||||
<option key={room.jid} value={room.jid}>
|
||||
{room.name} · {room.folder}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t.tasks.scheduleType}</span>
|
||||
<select name="scheduleType" required>
|
||||
<option value="once">{t.tasks.scheduleTypes.once}</option>
|
||||
<option value="interval">{t.tasks.scheduleTypes.interval}</option>
|
||||
<option value="cron">{t.tasks.scheduleTypes.cron}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t.tasks.scheduleValue}</span>
|
||||
<input
|
||||
name="scheduleValue"
|
||||
placeholder={t.tasks.scheduleValueHint}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t.tasks.context}</span>
|
||||
<select name="contextMode" required>
|
||||
<option value="isolated">{t.tasks.contextModes.isolated}</option>
|
||||
<option value="group">{t.tasks.contextModes.group}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="task-form-footer">
|
||||
<span>{t.tasks.schedule}</span>
|
||||
<button disabled={taskActionKey === 'create'} type="submit">
|
||||
{taskActionKey === 'create'
|
||||
? t.tasks.actions.busy
|
||||
: t.tasks.actions.create}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<span>{t.tasks.room}</span>
|
||||
<select name="roomJid" required>
|
||||
<option value="">{t.tasks.selectRoom}</option>
|
||||
{rooms.map((room) => (
|
||||
<option key={room.jid} value={room.jid}>
|
||||
{room.name} · {room.folder}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="task-form-wide">
|
||||
<span>{t.tasks.prompt}</span>
|
||||
<textarea
|
||||
name="prompt"
|
||||
placeholder={t.tasks.promptPlaceholder}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t.tasks.scheduleType}</span>
|
||||
<select name="scheduleType" required>
|
||||
<option value="once">{t.tasks.scheduleTypes.once}</option>
|
||||
<option value="interval">{t.tasks.scheduleTypes.interval}</option>
|
||||
<option value="cron">{t.tasks.scheduleTypes.cron}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t.tasks.scheduleValue}</span>
|
||||
<input
|
||||
name="scheduleValue"
|
||||
placeholder={t.tasks.scheduleValueHint}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t.tasks.context}</span>
|
||||
<select name="contextMode" required>
|
||||
<option value="isolated">{t.tasks.contextModes.isolated}</option>
|
||||
<option value="group">{t.tasks.contextModes.group}</option>
|
||||
</select>
|
||||
</label>
|
||||
<button disabled={taskActionKey === 'create'} type="submit">
|
||||
{taskActionKey === 'create'
|
||||
? t.tasks.actions.busy
|
||||
: t.tasks.actions.create}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -335,10 +466,10 @@ function TaskCreateForm({
|
||||
function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
|
||||
return (
|
||||
<div className="task-time-grid">
|
||||
<span>
|
||||
<span className="task-next-run">
|
||||
<small>{t.tasks.next}</small>
|
||||
<strong>{formatTaskDate(task.nextRun, locale)}</strong>
|
||||
<em>{formatRelativeDate(task.nextRun, locale, t)}</em>
|
||||
<strong>{formatRelativeDate(task.nextRun, locale, t)}</strong>
|
||||
<em>{formatTaskDate(task.nextRun, locale)}</em>
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.last}</small>
|
||||
@@ -347,8 +478,8 @@ function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.schedule}</small>
|
||||
<strong>{task.scheduleType}</strong>
|
||||
<em>{task.scheduleValue}</em>
|
||||
<strong>{taskScheduleText(task, t)}</strong>
|
||||
<em>{task.contextMode}</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -390,12 +521,23 @@ function TaskPromptDetails({ task, t }: { task: DashboardTask; t: Messages }) {
|
||||
<summary>{t.tasks.prompt}</summary>
|
||||
<p>{safePreview(task.promptPreview, t.tasks.emptyPrompt)}</p>
|
||||
<small>
|
||||
{task.id} · {task.contextMode} · {task.promptLength} {t.units.chars}
|
||||
{task.id} · {taskScheduleText(task, t)} · {task.promptLength}{' '}
|
||||
{t.units.chars}
|
||||
</small>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskMetaStrip({ task, t }: { task: DashboardTask; t: Messages }) {
|
||||
return (
|
||||
<div className="task-meta-strip">
|
||||
<span>{task.groupFolder}</span>
|
||||
<span>{task.agentType ?? t.tasks.task}</span>
|
||||
{task.ciProvider ? <span>{task.ciProvider}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskEditForm({
|
||||
onTaskUpdate,
|
||||
task,
|
||||
@@ -460,16 +602,14 @@ function TaskCard({
|
||||
<article className={`task-card task-card-${groupKey}`}>
|
||||
<div className="task-card-main">
|
||||
<div className="task-title">
|
||||
<strong>{taskDisplayName(task, t)}</strong>
|
||||
<span className="mono-chip">{task.groupFolder}</span>
|
||||
<span className="task-kind">{taskDisplayName(task, t)}</span>
|
||||
<strong>{taskHeadline(task, t)}</strong>
|
||||
<TaskMetaStrip task={task} t={t} />
|
||||
</div>
|
||||
<div className="task-status-line">
|
||||
<span className={`pill pill-${task.status}`}>
|
||||
{statusLabel(task.status, t)}
|
||||
</span>
|
||||
{task.ciProvider ? (
|
||||
<span className="task-provider">{task.ciProvider}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<TaskActionButtons
|
||||
@@ -546,6 +686,8 @@ function TaskGroupHead({
|
||||
|
||||
function TaskGroupSection(props: TaskGroupSectionProps) {
|
||||
const { group, t } = props;
|
||||
if (group.tasks.length === 0) return null;
|
||||
|
||||
const label = t.tasks.groups[group.key];
|
||||
const body = <TaskGroupBody {...props} />;
|
||||
|
||||
@@ -585,9 +727,11 @@ export function TaskPanel({
|
||||
t,
|
||||
}: TaskPanelProps) {
|
||||
const taskGroups = useMemo(() => buildTaskGroups(tasks), [tasks]);
|
||||
const taskSummary = useMemo(() => buildTaskSummary(tasks), [tasks]);
|
||||
|
||||
return (
|
||||
<div className="task-board" aria-label={t.tasks.cardsAria}>
|
||||
<TaskBoardSummary locale={locale} summary={taskSummary} t={t} />
|
||||
<TaskCreateForm
|
||||
rooms={rooms}
|
||||
onTaskCreate={onTaskCreate}
|
||||
@@ -596,17 +740,19 @@ export function TaskPanel({
|
||||
/>
|
||||
|
||||
{tasks.length === 0 ? <EmptyState>{t.tasks.empty}</EmptyState> : null}
|
||||
{taskGroups.map((group) => (
|
||||
<TaskGroupSection
|
||||
group={group}
|
||||
key={group.key}
|
||||
locale={locale}
|
||||
onTaskAction={onTaskAction}
|
||||
onTaskUpdate={onTaskUpdate}
|
||||
taskActionKey={taskActionKey}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
<div className="task-lanes">
|
||||
{taskGroups.map((group) => (
|
||||
<TaskGroupSection
|
||||
group={group}
|
||||
key={group.key}
|
||||
locale={locale}
|
||||
onTaskAction={onTaskAction}
|
||||
onTaskUpdate={onTaskUpdate}
|
||||
taskActionKey={taskActionKey}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -490,33 +490,33 @@ export const messages = {
|
||||
noResult: '결과 없음',
|
||||
noTime: '-',
|
||||
now: '지금',
|
||||
createTitle: 'Create task',
|
||||
createSubtitle: 'Schedule work from web',
|
||||
room: 'Room',
|
||||
selectRoom: 'Select room',
|
||||
scheduleType: 'Type',
|
||||
scheduleValue: 'Value',
|
||||
scheduleValueHint: 'ISO time, ms, or cron',
|
||||
promptPlaceholder: 'What should the agent do?',
|
||||
editPromptPlaceholder: 'Leave blank to keep prompt',
|
||||
createTitle: '작업 예약',
|
||||
createSubtitle: '룸에 반복 작업 걸기',
|
||||
room: '방',
|
||||
selectRoom: '방 선택',
|
||||
scheduleType: '방식',
|
||||
scheduleValue: '시간값',
|
||||
scheduleValueHint: 'ISO 시간, 10m 같은 간격, 또는 cron',
|
||||
promptPlaceholder: '에이전트가 실행할 작업을 적어주세요.',
|
||||
editPromptPlaceholder: '비우면 기존 프롬프트 유지',
|
||||
scheduleTypes: {
|
||||
once: 'Once',
|
||||
interval: 'Interval',
|
||||
cron: 'Cron',
|
||||
once: '한 번',
|
||||
interval: '간격',
|
||||
cron: '크론',
|
||||
},
|
||||
contextModes: {
|
||||
isolated: 'Isolated',
|
||||
group: 'Group',
|
||||
isolated: '격리',
|
||||
group: '방 맥락',
|
||||
},
|
||||
actions: {
|
||||
create: 'Create',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
pause: 'Pause',
|
||||
resume: 'Resume',
|
||||
cancel: 'Cancel',
|
||||
busy: 'Working',
|
||||
confirmCancel: 'Cancel this scheduled work?',
|
||||
create: '생성',
|
||||
edit: '수정',
|
||||
save: '저장',
|
||||
pause: '일시정지',
|
||||
resume: '재개',
|
||||
cancel: '취소',
|
||||
busy: '처리 중',
|
||||
confirmCancel: '이 예약 작업을 취소할까요?',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
|
||||
@@ -2439,404 +2439,6 @@ progress::-moz-progress-bar {
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.task-board {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-create-form,
|
||||
.task-edit-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.task-edit-form {
|
||||
grid-template-columns:
|
||||
minmax(0, 1.4fr) minmax(110px, 0.6fr) minmax(0, 1fr)
|
||||
auto;
|
||||
margin-top: 8px;
|
||||
padding: 9px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.task-create-head {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.task-create-head strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.task-create-form label,
|
||||
.task-edit-form label {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-create-form input,
|
||||
.task-create-form select,
|
||||
.task-create-form textarea,
|
||||
.task-edit-form input,
|
||||
.task-edit-form select,
|
||||
.task-edit-form textarea {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--panel-border-strong);
|
||||
border-radius: 13px;
|
||||
padding: 8px 10px;
|
||||
color: var(--ink);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-create-form textarea,
|
||||
.task-edit-form textarea {
|
||||
min-height: 38px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.task-create-form input:focus-visible,
|
||||
.task-create-form select:focus-visible,
|
||||
.task-create-form textarea:focus-visible,
|
||||
.task-edit-form input:focus-visible,
|
||||
.task-edit-form select:focus-visible,
|
||||
.task-edit-form textarea:focus-visible {
|
||||
outline: 3px solid rgba(191, 95, 44, 0.18);
|
||||
}
|
||||
|
||||
.task-create-form button,
|
||||
.task-edit-form button {
|
||||
min-height: 38px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 0 14px;
|
||||
color: var(--bg);
|
||||
background: var(--accent);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-create-form button:disabled,
|
||||
.task-edit-form button:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.task-form-wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.task-group-completed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.task-group-completed[open] {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.task-group-completed summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-group-completed summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-group-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-group-head strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 360px));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 14px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.task-card-main,
|
||||
.task-time-grid,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-title strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.02em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-status-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.task-provider {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--panel-border-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-action:hover,
|
||||
.task-action:focus-visible {
|
||||
border-color: rgba(191, 95, 44, 0.42);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(191, 95, 44, 0.12);
|
||||
}
|
||||
|
||||
.task-action:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.task-action.is-busy {
|
||||
display: inline-flex;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: progress;
|
||||
opacity: 0.85;
|
||||
background: rgba(191, 95, 44, 0.12);
|
||||
border-color: rgba(191, 95, 44, 0.32);
|
||||
}
|
||||
|
||||
.task-action.is-busy::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: ej-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ej-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.task-action-cancel {
|
||||
color: #8f2f22;
|
||||
border-color: rgba(183, 71, 52, 0.22);
|
||||
background: rgba(183, 71, 52, 0.08);
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.task-time-grid > span,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
padding: 9px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.task-time-grid small,
|
||||
.task-suspended span,
|
||||
.task-result span,
|
||||
.task-time-grid em,
|
||||
.task-suspended em {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-time-grid small,
|
||||
.task-time-grid strong,
|
||||
.task-time-grid em,
|
||||
.task-suspended span,
|
||||
.task-suspended strong,
|
||||
.task-suspended em,
|
||||
.task-result span,
|
||||
.task-result strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-time-grid strong,
|
||||
.task-suspended strong,
|
||||
.task-result strong {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.task-suspended {
|
||||
border-color: rgba(181, 138, 34, 0.22);
|
||||
background: rgba(181, 138, 34, 0.08);
|
||||
}
|
||||
|
||||
.task-result {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border-color: rgba(63, 127, 81, 0.15);
|
||||
}
|
||||
|
||||
.result-fail {
|
||||
border-color: rgba(183, 71, 52, 0.28);
|
||||
background: rgba(183, 71, 52, 0.08);
|
||||
}
|
||||
|
||||
.result-none {
|
||||
border-color: rgba(109, 115, 95, 0.14);
|
||||
}
|
||||
|
||||
.task-prompt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.task-edit summary,
|
||||
.task-prompt summary {
|
||||
min-height: 32px;
|
||||
color: var(--accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-prompt p {
|
||||
margin: 4px 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-prompt small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-family:
|
||||
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-group-empty {
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.pill-watchers {
|
||||
color: #173e23;
|
||||
background: rgba(63, 127, 81, 0.18);
|
||||
}
|
||||
|
||||
.pill-scheduled {
|
||||
color: #6e4a05;
|
||||
background: rgba(181, 138, 34, 0.16);
|
||||
}
|
||||
|
||||
.pill-paused {
|
||||
color: #6e4a05;
|
||||
background: rgba(181, 138, 34, 0.2);
|
||||
}
|
||||
|
||||
.pill-completed {
|
||||
color: #625d52;
|
||||
background: rgba(109, 115, 95, 0.16);
|
||||
}
|
||||
|
||||
.task-card > p {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mono-chip {
|
||||
font-family:
|
||||
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
||||
@@ -3165,55 +2767,6 @@ progress::-moz-progress-bar {
|
||||
.health-service > div:nth-of-type(3) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task-create-form,
|
||||
.task-edit-form {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 7px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task-form-wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
gap: 7px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
min-height: 44px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.task-card-main {
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-time-grid > span,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.task-prompt summary {
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
@@ -3270,14 +2823,6 @@ progress::-moz-progress-bar {
|
||||
.health-restart-record {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.task-time-grid > span:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
|
||||
Reference in New Issue
Block a user