From deb53d914a28e77faeeddae89321a9f368119750 Mon Sep 17 00:00:00 2001 From: jackwener Date: Mon, 3 Aug 2026 19:34:51 +0800 Subject: [PATCH] redesign(ui): rebuild the plan-reminder form as a grouped-row panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The create/edit dialog packed nine fields into paired two-column FormLayouts under an eyebrow heading — dense, boxy, and unlike any other Maka surface. Rebuilt on the reference scheduled-task panel shape, expressed in Maka's own idiom: - a quiet kicker (新建提醒 / 编辑提醒) with the template menu and close beside it, then ONE display element: a large borderless title input (raw wrapped in Astryx Field so the hidden label and detached error status stay accessible), then the bare description textarea; - the schedule and delivery fields become two labeled groups of label-left / control-right hairline rows (重复 / 时间 / Cron, then 方式 / 平台 / Chat ID) — the settings open-row idiom scoped to the dialog — with the run-at presets as a quiet ghost row under 时间; - the title's "required" error no longer scolds an untouched empty form: it appears after first input (the submit button was already the gate); - copy gains groupSchedule/groupDelivery + short row labels (时间 / 方式) so group and row labels stop repeating each other; the unused eyebrow style is retired. All form state, validation, and the submit single-flight/close-guard invariants are unchanged — presentation only. Verified: build, typecheck, check-dead-css, check-a11y/copy/console, product Storybook smoke (71 renders x 3 viewports), plus a rendered screenshot of the open dialog from the built Storybook. --- .../styles/module-pages/plan-reminders.css | 72 ++++- packages/ui/src/plan-reminder-copy.ts | 8 +- packages/ui/src/plan-reminder-form-dialog.tsx | 273 +++++++++++------- 3 files changed, 228 insertions(+), 125 deletions(-) diff --git a/apps/desktop/src/renderer/styles/module-pages/plan-reminders.css b/apps/desktop/src/renderer/styles/module-pages/plan-reminders.css index 377732e712..cbf6eb3a82 100644 --- a/apps/desktop/src/renderer/styles/module-pages/plan-reminders.css +++ b/apps/desktop/src/renderer/styles/module-pages/plan-reminders.css @@ -41,12 +41,6 @@ max-width: 560px; } -.maka-plan-eyebrow { - font: var(--maka-text-heading-5); - margin: 0; - color: var(--muted-foreground); - letter-spacing: var(--tracking-normal); -} .maka-plan-heading h2 { font: var(--maka-text-display-2); @@ -173,7 +167,8 @@ border: 0; border-radius: 0; background: var(--background); - padding: var(--space-3); + gap: var(--space-4); + padding: var(--space-4) var(--space-5) var(--space-4); overflow: auto; } @@ -188,15 +183,70 @@ gap: var(--space-1); } -.maka-plan-form-title { - font: var(--maka-text-heading-4); +.maka-plan-form-kicker { + font: var(--maka-text-supporting); + margin: 0; + color: var(--muted-foreground); +} + +/* The large borderless title — the form's one display element. A raw input + wrapped in Astryx Field (label + status stay accessible); chrome-free so + the dialog opens straight into naming the task, like the reference panel. */ +.maka-plan-title-input { + font: var(--maka-text-heading-2); + width: 100%; margin: 0; + padding: 0; + border: 0; + outline: 0; + background: transparent; color: var(--foreground); } -.maka-plan-presets { +.maka-plan-title-input::placeholder { + color: var(--muted-foreground); +} + +/* One labeled group: quiet label over hairline-divided label/control rows — + the settings open-row idiom, scoped to the dialog. */ +.maka-plan-group { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-1); +} + +.maka-plan-group-label { + font: var(--maka-text-supporting); + margin: 0; + color: var(--muted-foreground); +} + +.maka-plan-rows { + display: grid; + grid-template-columns: minmax(0, 1fr); +} + +.maka-plan-rows > * + * { + border-block-start: var(--border-width-hairline) solid var(--border); +} + +.maka-plan-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--space-2) var(--space-3); + min-height: 44px; + padding-block: var(--space-1-5); +} + +.maka-plan-row-label { + font: var(--maka-text-body); + color: var(--foreground); +} + +.maka-plan-presets { + display: flex; + flex-wrap: wrap; gap: var(--space-1); } diff --git a/packages/ui/src/plan-reminder-copy.ts b/packages/ui/src/plan-reminder-copy.ts index 4023e7c646..8797b35f87 100644 --- a/packages/ui/src/plan-reminder-copy.ts +++ b/packages/ui/src/plan-reminder-copy.ts @@ -58,7 +58,9 @@ export interface PlanReminderCopy { close: string; useTemplate: string; templatesAriaLabel: string; - field: { title: string; time: string; recurrence: string; delivery: string; platform: string; cron: string; chatId: string; note: string }; + groupSchedule: string; + groupDelivery: string; + field: { title: string; time: string; at: string; channel: string; recurrence: string; delivery: string; platform: string; cron: string; chatId: string; note: string }; timePlaceholder: string; titlePlaceholder: string; presetsAriaLabel: string; @@ -148,7 +150,7 @@ const PLAN_REMINDER_COPY = { runStatus: { triggered: '已触发', blocked: '已阻止', failed: '失败' }, delivery: { local: '本地提醒', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `触发后投递到:${target}` }, form: { - eyebrow: '计划提示词', editTitle: '编辑提醒', createTitle: '新建提醒', close: '关闭计划提醒表单', useTemplate: '使用模板', templatesAriaLabel: '定时任务模板', field: { title: '标题', time: '提醒时间', recurrence: '重复', delivery: '投递', platform: '平台', cron: 'Cron', chatId: 'Chat ID', note: '备注' }, timePlaceholder: 'YYYY-MM-DDTHH:mm', titlePlaceholder: '例如:明天复盘项目进度', presetsAriaLabel: '快速设置提醒时间', presets: [['ten-minutes', '10 分钟后'], ['one-hour', '1 小时后'], ['tomorrow-morning', '明天 9 点'], ['next-monday', '下周一 9 点']], recurrenceOptions: [['none', '不重复'], ['daily', '每天'], ['weekly', '每周'], ['monthly', '每月'], ['cron', 'Cron']], deliveryOptions: [['local', '本地提醒'], ['bot', '机器人聊天']], cronPlaceholder: '例如 0 9 * * 1-5', chatIdPlaceholder: '例如 Telegram chat_id', deliveryHelp: (providers) => `当前可投递到 ${providers};其它机器人平台不会出现在投递目标里。`, notePlaceholder: '可选:补充需要提醒的上下文', cancel: '取消', saving: '保存中…', creating: '创建中…', save: '保存提醒', create: '创建提醒', + eyebrow: '计划提示词', editTitle: '编辑提醒', createTitle: '新建提醒', close: '关闭计划提醒表单', useTemplate: '使用模板', templatesAriaLabel: '定时任务模板', groupSchedule: '频率', groupDelivery: '投递', field: { title: '标题', time: '提醒时间', at: '时间', channel: '方式', recurrence: '重复', delivery: '投递', platform: '平台', cron: 'Cron', chatId: 'Chat ID', note: '备注' }, timePlaceholder: 'YYYY-MM-DDTHH:mm', titlePlaceholder: '例如:明天复盘项目进度', presetsAriaLabel: '快速设置提醒时间', presets: [['ten-minutes', '10 分钟后'], ['one-hour', '1 小时后'], ['tomorrow-morning', '明天 9 点'], ['next-monday', '下周一 9 点']], recurrenceOptions: [['none', '不重复'], ['daily', '每天'], ['weekly', '每周'], ['monthly', '每月'], ['cron', 'Cron']], deliveryOptions: [['local', '本地提醒'], ['bot', '机器人聊天']], cronPlaceholder: '例如 0 9 * * 1-5', chatIdPlaceholder: '例如 Telegram chat_id', deliveryHelp: (providers) => `当前可投递到 ${providers};其它机器人平台不会出现在投递目标里。`, notePlaceholder: '可选:补充需要提醒的上下文', cancel: '取消', saving: '保存中…', creating: '创建中…', save: '保存提醒', create: '创建提醒', }, page: { title: '定时任务', subtitle: '创建和管理周期性任务,让 Maka 按计划执行提醒、复盘和投递。', actionsAriaLabel: '计划提醒操作', refreshing: '正在刷新定时任务', refresh: '刷新定时任务', create: '新建定时任务', keepAwake: '保持系统唤醒', pageSettings: '定时任务页面设置', keepAwakeErrorTitle: '无法更新保持系统唤醒', keepAwakeErrorFallback: '更新保持系统唤醒设置失败,请稍后重试。', viewsAriaLabel: '计划提醒视图', tasks: '我的定时任务', runs: '执行记录', filtersAriaLabel: '计划提醒筛选', sort: '排序', sortOptions: [['created-desc', '按创建时间倒序'], ['next-run-asc', '按下次触发升序'], ['updated-desc', '按更新时间倒序']], searchLabel: '搜索计划提醒', searchPlaceholder: '搜索标题、备注、投递或执行记录…', state: '状态', filterOption: (label, count) => `${label} ${count}`, active: '进行中', all: '全部', runsFilterAriaLabel: '执行记录筛选', range: '范围', rangeOptions: [['day', '今天'], ['week', '近 7 天'], ['month', '近 30 天'], ['all', '全部记录']], searchMatches: (count) => `找到 ${count} 个匹配提醒`, clearSearch: '清除搜索', noSearchTitle: '没有匹配的提醒', noFilterTitle: '当前筛选没有提醒', noSearchBody: '调整搜索词,或切换状态筛选查看其他提醒。', noFilterBody: '切换筛选查看其他状态,或创建新的计划提醒。', emptyTitle: '还没有定时任务', emptyBody: '创建一个提醒,让 Maka 在指定时间继续这项工作。', listAriaLabel: '计划提醒列表', pause: '暂停提醒', enable: '启用提醒', reminderActions: '提醒操作', edit: '编辑', duplicate: '复制', triggering: '触发中…', triggerNow: '立即触发', snoozing: '延后中…', snooze: '延后 10 分钟', clearing: '清空中…', clearRuns: '清空记录', deleting: '删除中…', delete: '删除', nextRun: (time) => `下次触发:${time}`, recentRun: (time) => `最近 ${time}`, unscheduled: '未安排', noRunsTitle: '暂无执行记录', noRunsBody: '提醒触发、手动执行或投递失败后,会在这里保留最近记录。', runsAriaLabel: '计划提醒执行记录', @@ -169,7 +171,7 @@ const PLAN_REMINDER_COPY = { runStatus: { triggered: 'Triggered', blocked: 'Blocked', failed: 'Failed' }, delivery: { local: 'Local notification', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `Deliver to: ${target}` }, form: { - eyebrow: 'Task prompt', editTitle: 'Edit reminder', createTitle: 'New reminder', close: 'Close reminder form', useTemplate: 'Use template', templatesAriaLabel: 'Scheduled task templates', field: { title: 'Title', time: 'Reminder time', recurrence: 'Repeat', delivery: 'Delivery', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, timePlaceholder: 'YYYY-MM-DDTHH:mm', titlePlaceholder: 'For example: Review project progress tomorrow', presetsAriaLabel: 'Quick reminder times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this reminder', cancel: 'Cancel', saving: 'Saving…', creating: 'Creating…', save: 'Save reminder', create: 'Create reminder', + eyebrow: 'Task prompt', editTitle: 'Edit reminder', createTitle: 'New reminder', close: 'Close reminder form', useTemplate: 'Use template', templatesAriaLabel: 'Scheduled task templates', groupSchedule: 'Frequency', groupDelivery: 'Delivery', field: { title: 'Title', time: 'Reminder time', at: 'Time', channel: 'Method', recurrence: 'Repeat', delivery: 'Delivery', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, timePlaceholder: 'YYYY-MM-DDTHH:mm', titlePlaceholder: 'For example: Review project progress tomorrow', presetsAriaLabel: 'Quick reminder times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this reminder', cancel: 'Cancel', saving: 'Saving…', creating: 'Creating…', save: 'Save reminder', create: 'Create reminder', }, page: { title: 'Scheduled tasks', subtitle: 'Create and manage recurring tasks so Maka can run reminders, reviews, and deliveries on schedule.', actionsAriaLabel: 'Scheduled task actions', refreshing: 'Refreshing scheduled tasks', refresh: 'Refresh scheduled tasks', create: 'New scheduled task', keepAwake: 'Keep system awake', pageSettings: 'Scheduled task page settings', keepAwakeErrorTitle: 'Could not update Keep system awake', keepAwakeErrorFallback: 'Could not update the Keep system awake setting. Try again later.', viewsAriaLabel: 'Scheduled task views', tasks: 'My scheduled tasks', runs: 'Run history', filtersAriaLabel: 'Scheduled task filters', sort: 'Sort', sortOptions: [['created-desc', 'Newest created first'], ['next-run-asc', 'Next run first'], ['updated-desc', 'Recently updated first']], searchLabel: 'Search scheduled tasks', searchPlaceholder: 'Search titles, notes, delivery, or run history…', state: 'Status', filterOption: (label, count) => `${label} ${count}`, active: 'Active', all: 'All', runsFilterAriaLabel: 'Run history filters', range: 'Range', rangeOptions: [['day', 'Today'], ['week', 'Last 7 days'], ['month', 'Last 30 days'], ['all', 'All runs']], searchMatches: (count) => `${count} matching ${count === 1 ? 'reminder' : 'reminders'}`, clearSearch: 'Clear search', noSearchTitle: 'No matching reminders', noFilterTitle: 'No reminders in this filter', noSearchBody: 'Change the search terms or status filter to find other reminders.', noFilterBody: 'Change the filter or create a new scheduled task.', emptyTitle: 'No scheduled tasks yet', emptyBody: 'Create a reminder so Maka can continue this work at the right time.', listAriaLabel: 'Scheduled task list', pause: 'Pause reminder', enable: 'Enable reminder', reminderActions: 'Reminder actions', edit: 'Edit', duplicate: 'Duplicate', triggering: 'Triggering…', triggerNow: 'Trigger now', snoozing: 'Snoozing…', snooze: 'Snooze 10 minutes', clearing: 'Clearing…', clearRuns: 'Clear history', deleting: 'Deleting…', delete: 'Delete', nextRun: (time) => `Next run: ${time}`, recentRun: (time) => `Last run ${time}`, unscheduled: 'Not scheduled', noRunsTitle: 'No run history', noRunsBody: 'Triggered reminders, manual runs, and delivery failures appear here.', runsAriaLabel: 'Scheduled task run history', diff --git a/packages/ui/src/plan-reminder-form-dialog.tsx b/packages/ui/src/plan-reminder-form-dialog.tsx index a4f94a45a0..7aa0dfedf1 100644 --- a/packages/ui/src/plan-reminder-form-dialog.tsx +++ b/packages/ui/src/plan-reminder-form-dialog.tsx @@ -36,7 +36,7 @@ import { } from './plan-reminder-helpers.js'; import { Button as UiButton, - FormLayout, + Field, Selector, TextInput, } from '@astryxdesign/core'; @@ -81,6 +81,9 @@ export function PlanReminderFormDialog(props: { const [deliveryChatId, setDeliveryChatId] = useState(props.seed.deliveryChatId); const [editingId, setEditingId] = useState(props.seed.editingId); const [submitPending, setSubmitPending] = useState(false); + // The empty form is invalid by definition; showing the title error before + // the user has typed anything reads as a scolding. Gate it on first input. + const [titleTouched, setTitleTouched] = useState(false); const planReminderMountedRef = useMountedRef(); const submitPendingRef = useRef(false); const parsedRunAt = Date.parse(runAtLocal); @@ -120,6 +123,7 @@ export function PlanReminderFormDialog(props: { setDeliveryChatId(''); setRunAtLocal(toPlanReminderLocalDateTimeValue(Date.now() + 60 * 60 * 1000)); setEditingId(null); + setTitleTouched(false); } function closeReminderDialog() { @@ -194,12 +198,14 @@ export function PlanReminderFormDialog(props: { + {/* Redesign (WAWQAQ msg `67d21f99`): the form reads like the reference + scheduled-task panels now — a quiet kicker, a large borderless + title, a bare description field, then label-left / control-right + hairline rows in two labeled groups. All state, validation and + submit invariants are unchanged; only the presentation moved. */}
-
-

{copy.eyebrow}

-

{isEditing ? copy.editTitle : copy.createTitle}

-
+

{isEditing ? copy.editTitle : copy.createTitle}

{!isEditing && (
- - + setTitle(value.slice(0, 120))} - data-maka-plan-title-input="true" + autoFocus + onChange={(event) => { + setTitleTouched(true); + setTitle(event.target.value.slice(0, 120)); + }} placeholder={copy.titlePlaceholder} - isDisabled={formInteractionDisabled} - isRequired - status={validation?.field === 'title' - ? { type: 'error', message: validation.message } - : undefined} - /> - - -
- {copy.presets.map(([preset, label]) => ( - applyRunAtPreset(preset)} - isDisabled={formInteractionDisabled} - label={label} - /> - ))} -
- - setRecurrence(value as PlanReminderRecurrence)} - isDisabled={formInteractionDisabled} - label={copy.field.recurrence} - width="100%" - options={copy.recurrenceOptions.map(([value, label]) => ({ value, label }))} - /> - setDeliveryChannel(value as PlanReminderDeliveryTarget['channel'])} - isDisabled={formInteractionDisabled} - label={copy.field.delivery} - width="100%" - options={copy.deliveryOptions.map(([value, label]) => ({ value, label }))} - /> - - {recurrence === 'cron' && ( - setCronExpression(value.slice(0, 80))} - placeholder={copy.cronPlaceholder} - isDisabled={formInteractionDisabled} - isRequired - status={validation?.field === 'cron' - ? { type: 'error', message: validation.message } - : undefined} + disabled={formInteractionDisabled} + data-maka-plan-title-input="true" /> - )} - {deliveryChannel === 'bot' && ( - <> - + + setNote(value.slice(0, 1000))} + maxLength={1000} + rows={3} + placeholder={copy.notePlaceholder} + isDisabled={formInteractionDisabled} + /> +
+

{copy.groupSchedule}

+
+
+ {copy.field.recurrence} setDeliveryPlatform(value as BotProvider)} + value={recurrence} + onChange={(value) => setRecurrence(value as PlanReminderRecurrence)} isDisabled={formInteractionDisabled} - label={copy.field.platform} - width="100%" - options={BOT_DELIVERY_PROVIDERS.map((provider) => { - const icon = ( -
+
+ {copy.field.at} setDeliveryChatId(value.slice(0, 160))} - placeholder={copy.chatIdPlaceholder} + label={copy.field.time} + isLabelHidden + value={runAtLocal} + onChange={setRunAtLocal} + placeholder={copy.timePlaceholder} isDisabled={formInteractionDisabled} isRequired - status={validation?.field === 'chatId' + width={220} + status={validation?.field === 'time' ? { type: 'error', message: validation.message } : undefined} /> - +
+ {recurrence === 'cron' && ( +
+ {copy.field.cron} + setCronExpression(value.slice(0, 80))} + placeholder={copy.cronPlaceholder} + isDisabled={formInteractionDisabled} + isRequired + width={220} + status={validation?.field === 'cron' + ? { type: 'error', message: validation.message } + : undefined} + /> +
+ )} +
+
+ {copy.presets.map(([preset, label]) => ( + applyRunAtPreset(preset)} + isDisabled={formInteractionDisabled} + label={label} + /> + ))} +
+
+
+

{copy.groupDelivery}

+
+
+ {copy.field.channel} + setDeliveryChannel(value as PlanReminderDeliveryTarget['channel'])} + isDisabled={formInteractionDisabled} + label={copy.field.delivery} + isLabelHidden + width={220} + options={copy.deliveryOptions.map(([value, label]) => ({ value, label }))} + /> +
+ {deliveryChannel === 'bot' && ( + <> +
+ {copy.field.platform} + setDeliveryPlatform(value as BotProvider)} + isDisabled={formInteractionDisabled} + label={copy.field.platform} + isLabelHidden + width={220} + options={BOT_DELIVERY_PROVIDERS.map((provider) => { + const icon = ( +
+
+ {copy.field.chatId} + setDeliveryChatId(value.slice(0, 160))} + placeholder={copy.chatIdPlaceholder} + isDisabled={formInteractionDisabled} + isRequired + width={220} + status={validation?.field === 'chatId' + ? { type: 'error', message: validation.message } + : undefined} + /> +
+ + )} +
+ {deliveryChannel === 'bot' && (

{copy.deliveryHelp(formatPlanDeliveryProviderList())}

- - )} - setNote(value.slice(0, 1000))} - maxLength={1000} - rows={5} - placeholder={copy.notePlaceholder} - isDisabled={formInteractionDisabled} - /> + )} +