diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index ae39fcf..09c692d 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -36,7 +36,7 @@ '.files-breadcrumb-sep{color:var(--vt-color-text-muted,#7f8aa3)}', '.files-filter,.files-sort,.files-create-input,.files-rename-input{font-size:.78rem;padding:.32rem .5rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);outline:none}', '.files-filter{width:11rem}', - '.files-sort{width:9.5rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}', + '.files-sort{width:9.5rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}.files-sort option{background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb)}', '.files-filter:focus,.files-sort:focus,.files-create-input:focus,.files-rename-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', '.files-list{flex:1;overflow:auto;min-height:0}', '.files-header,.files-item{display:grid;grid-template-columns:minmax(160px,1fr) 90px 90px 150px 220px;align-items:center;gap:.5rem;padding:.38rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}', diff --git a/plugins/notes/frontend/src/index.js b/plugins/notes/frontend/src/index.js index f1feb15..5013921 100644 --- a/plugins/notes/frontend/src/index.js +++ b/plugins/notes/frontend/src/index.js @@ -25,7 +25,7 @@ '.notes-btn.primary:hover{background:#63d9b3;color:#101827}', '.notes-filter,.notes-sort{font-size:.78rem;padding:.32rem .5rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);outline:none}', '.notes-filter{width:11rem}', - '.notes-sort{width:8rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}', + '.notes-sort{width:8rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}.notes-sort option{background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb)}', '.notes-filter:focus,.notes-sort:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', '.notes-list{flex:1;overflow:auto;min-height:0}', '.notes-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid rgba(32,43,70,.72);cursor:pointer;font-size:.85rem}', diff --git a/plugins/secrets/frontend/src/index.js b/plugins/secrets/frontend/src/index.js index df17129..e8094ba 100644 --- a/plugins/secrets/frontend/src/index.js +++ b/plugins/secrets/frontend/src/index.js @@ -45,7 +45,7 @@ '.secrets-row{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.65rem;align-items:center}', '.secrets-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}', '.secrets-input,.secrets-textarea,.secrets-select{width:100%;box-sizing:border-box;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.84rem;padding:.45rem .55rem;outline:none}', - '.secrets-select{appearance:none;background-color:#0d1117;background-image:linear-gradient(45deg,transparent 50%,#8b949e 50%),linear-gradient(135deg,#8b949e 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:2rem}', + '.secrets-select{appearance:none;background-color:#0d1117;background-image:linear-gradient(45deg,transparent 50%,#8b949e 50%),linear-gradient(135deg,#8b949e 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:2rem}.secrets-select option{background:#0d1117;color:var(--vt-color-text-primary,#f4f7fb)}', '.secrets-textarea{min-height:6rem;resize:vertical;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}', '.secrets-input:focus,.secrets-textarea:focus,.secrets-select:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', '.secrets-actions{display:flex;gap:.5rem;flex-wrap:wrap}', diff --git a/plugins/todo/frontend/src/index.js b/plugins/todo/frontend/src/index.js index 4b2e3b0..dfece9f 100644 --- a/plugins/todo/frontend/src/index.js +++ b/plugins/todo/frontend/src/index.js @@ -17,8 +17,8 @@ '.todo-toolbar{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629);flex-wrap:wrap;flex-shrink:0}', '.todo-title{font-size:.86rem;font-weight:600}.todo-count,.todo-status,.todo-scope{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3)}.todo-spacer{flex:1}', '.todo-filters{display:flex;align-items:center;gap:.35rem;min-width:0;flex:1;flex-wrap:wrap}', - '.todo-input,.todo-select{box-sizing:border-box;min-height:1.9rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.78rem;padding:.32rem .45rem}', - '.todo-input.search{width:min(15rem,100%)}.todo-input.textarea{min-height:6.5rem;resize:vertical;line-height:1.4}.todo-select{max-width:12rem}', + '.todo-input,.todo-select{box-sizing:border-box;min-height:1.9rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);color-scheme:dark;font:inherit;font-size:.78rem;padding:.32rem .45rem}', + '.todo-input.search{width:min(15rem,100%)}.todo-input.textarea{min-height:6.5rem;resize:vertical;line-height:1.4}.todo-select{max-width:12rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.todo-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}', '.todo-btn{min-height:1.9rem;padding:.32rem .62rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);font-size:.78rem;cursor:pointer}.todo-btn:hover{border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}.todo-btn.primary{background:var(--vt-color-accent,#4ecca3);border-color:var(--vt-color-accent,#4ecca3);color:#101827}.todo-btn.danger{border-color:rgba(233,69,96,.5);color:#ff9a9a}.todo-btn:disabled{opacity:.45;cursor:default}', '.todo-list{flex:1;min-height:0;overflow:auto;padding:.5rem .75rem .85rem}.todo-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:2rem;text-align:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem}', '.todo-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem;align-items:start;margin-top:.5rem;padding:.75rem .85rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}.todo-row:hover{background:var(--vt-color-surface-hover,#1b2440)}.todo-row.done .todo-row-title{text-decoration:line-through;color:var(--vt-color-text-muted,#7f8aa3)}', @@ -96,7 +96,28 @@ } function cleanDate(value) { - return /^\d{4}-\d{2}-\d{2}$/.test(text(value).trim()) ? text(value).trim() : ''; + value = text(value).trim(); + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value; + + var match = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(value); + var day; + var month; + var year; + if (match) { + month = Number(match[1]); + day = Number(match[2]); + year = Number(match[3]); + } else { + match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(value); + if (!match) return ''; + day = Number(match[1]); + month = Number(match[2]); + year = Number(match[3]); + } + + var date = new Date(Date.UTC(year, month - 1, day)); + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return ''; + return String(year).padStart(4, '0') + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0'); } function cleanDateTime(value) { @@ -106,6 +127,18 @@ return isNaN(date.getTime()) ? '' : value; } + function splitReminderDateTime(value) { + var match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(cleanDateTime(value)); + return match ? { date: match[1], time: match[2] } : { date: '', time: '' }; + } + + function joinReminderDateTime(date, time) { + date = cleanDate(date); + time = text(time).trim(); + if (!date || !/^\d{2}:\d{2}$/.test(time)) return ''; + return cleanDateTime(date + 'T' + time); + } + function todoId(workspaceRoot, title) { return 'todo:' + (cleanWorkspace(workspaceRoot) || 'global') + ':' + Date.now() + ':' + Math.random().toString(36).slice(2, 8) + ':' + text(title).trim().slice(0, 20).replace(/\s+/g, '-'); } @@ -413,7 +446,9 @@ var priorityInput = el('select', { className: 'todo-select', 'data-todo-input': 'priority' }, [option('low', tr('ui.priority.low', null, 'Low')), option('normal', tr('ui.priority.normal', null, 'Normal')), option('high', tr('ui.priority.high', null, 'High'))]); priorityInput.value = editing ? existingTodo.priority : 'normal'; var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' }); - var reminderInput = el('input', { className: 'todo-input', type: 'datetime-local', value: editing ? existingTodo.reminderAt : '', 'data-todo-input': 'reminderAt' }); + var reminder = splitReminderDateTime(editing ? existingTodo.reminderAt : ''); + var reminderDateInput = el('input', { className: 'todo-input', type: 'date', value: reminder.date, 'data-todo-input': 'reminderDate' }); + var reminderTimeInput = el('input', { className: 'todo-input', type: 'time', value: reminder.time, 'data-todo-input': 'reminderTime' }); var workspaceInput = null; var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot; if (scope.mode === 'global') { @@ -444,7 +479,7 @@ status: editing ? existingTodo.status : 'open', priority: priorityInput.value, dueAt: dueInput.value, - reminderAt: reminderInput.value, + reminderAt: joinReminderDateTime(reminderDateInput.value, reminderTimeInput.value), createdAt: editing ? existingTodo.createdAt : timestamp, updatedAt: timestamp, completedAt: editing ? existingTodo.completedAt : '', @@ -469,7 +504,8 @@ el('label', { className: 'todo-field wide' }, [tr('ui.field.description', null, 'Description'), descriptionInput]), el('label', { className: 'todo-field' }, [tr('ui.field.priority', null, 'Priority'), priorityInput]), el('label', { className: 'todo-field' }, [tr('ui.field.due', null, 'Due date'), dueInput]), - el('label', { className: 'todo-field' }, [tr('ui.field.reminder', null, 'Reminder'), reminderInput]) + el('label', { className: 'todo-field' }, [tr('ui.field.reminderDate', null, 'Reminder date'), reminderDateInput]), + el('label', { className: 'todo-field' }, [tr('ui.field.reminderTime', null, 'Reminder time'), reminderTimeInput]) ]; if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, [tr('ui.field.workspace', null, 'Workspace'), workspaceInput])); else fields.push(el('div', { className: 'todo-field', textContent: tr('ui.workspaceValue', { workspace: scope.workspaceRoot }, 'Workspace: ' + scope.workspaceRoot) })); diff --git a/plugins/todo/locales/en.json b/plugins/todo/locales/en.json index eed0874..ccf5faa 100644 --- a/plugins/todo/locales/en.json +++ b/plugins/todo/locales/en.json @@ -35,6 +35,8 @@ "ui.field.priority": "Priority", "ui.field.due": "Due date", "ui.field.reminder": "Reminder", + "ui.field.reminderDate": "Reminder date", + "ui.field.reminderTime": "Reminder time", "ui.field.workspace": "Workspace", "ui.workspaceValue": "Workspace: {workspace}", "ui.edit": "Edit Todo", diff --git a/plugins/todo/locales/ru.json b/plugins/todo/locales/ru.json index 2c6a7e8..7da7902 100644 --- a/plugins/todo/locales/ru.json +++ b/plugins/todo/locales/ru.json @@ -35,6 +35,8 @@ "ui.field.priority": "Приоритет", "ui.field.due": "Срок", "ui.field.reminder": "Напоминание", + "ui.field.reminderDate": "Дата напоминания", + "ui.field.reminderTime": "Время напоминания", "ui.field.workspace": "Рабочее пространство", "ui.workspaceValue": "Рабочее пространство: {workspace}", "ui.edit": "Изменить задачу", diff --git a/plugins/trash/frontend/src/index.js b/plugins/trash/frontend/src/index.js index 0067147..4641f9b 100644 --- a/plugins/trash/frontend/src/index.js +++ b/plugins/trash/frontend/src/index.js @@ -16,7 +16,7 @@ '.trash-spacer{flex:1}', '.trash-control{min-height:2rem;box-sizing:border-box;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.78rem;padding:.35rem .5rem}', '.trash-search{min-width:12rem;flex:1 1 14rem}', - '.trash-select{max-width:11rem}', + '.trash-select{max-width:11rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.trash-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}.trash-control:focus{outline:none;border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}', '.trash-btn{min-height:2rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);font:inherit;font-size:.78rem;padding:.35rem .6rem;cursor:pointer;white-space:nowrap}', '.trash-btn:hover:not(:disabled){border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}', '.trash-btn:disabled{opacity:.48;cursor:default}', diff --git a/scripts/check-select-styles.js b/scripts/check-select-styles.js new file mode 100644 index 0000000..a951a79 --- /dev/null +++ b/scripts/check-select-styles.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const checks = [ + ['plugins/todo/frontend/src/index.js', '.todo-select'], + ['plugins/files/frontend/src/index.js', '.files-sort'], + ['plugins/notes/frontend/src/index.js', '.notes-sort'], + ['plugins/browser-inbox/frontend/src/index.js', '.browser-inbox-select'], + ['plugins/trash/frontend/src/index.js', '.trash-select'], + ['plugins/secrets/frontend/src/index.js', '.secrets-select'], +]; + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +let failed = false; +for (const [relativePath, selector] of checks) { + const source = fs.readFileSync(path.join(root, relativePath), 'utf8'); + const escaped = escapeRegExp(selector); + if (!new RegExp(`${escaped}\\{[^}]*appearance:none`).test(source)) { + console.error(`${relativePath}: ${selector} must hide the native select arrow`); + failed = true; + } + if (!new RegExp(`${escaped} option\\{[^}]*background`).test(source)) { + console.error(`${relativePath}: ${selector} options must use the application surface`); + failed = true; + } +} + +if (failed) process.exit(1); +console.log('official plugin select styles are complete'); diff --git a/scripts/check.sh b/scripts/check.sh index 1ceafe6..8412f33 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -89,6 +89,18 @@ else echo " ⚠️ node not available — skipping localization catalog validation" fi +echo "" +echo "[select styles]" +if command -v node &>/dev/null; then + set +e + node "$ROOT/scripts/check-select-styles.js" + STATUS=$? + set -e + report "selects use application styles" "$STATUS" +else + echo " ⚠️ node not available — skipping select style validation" +fi + echo "" # Guard official plugins against bypassing the v2 plugin API for note features. echo "[frontend API boundary]" diff --git a/scripts/smoke-todo-plugin.js b/scripts/smoke-todo-plugin.js index b6c107b..fedb805 100755 --- a/scripts/smoke-todo-plugin.js +++ b/scripts/smoke-todo-plugin.js @@ -231,8 +231,13 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make byData(container, 'data-todo-input', 'title').value = 'Prepare project review'; byData(container, 'data-todo-input', 'description').value = 'Collect factual review notes.'; byData(container, 'data-todo-input', 'priority').value = 'high'; - byData(container, 'data-todo-input', 'dueAt').value = '2000-01-01'; - byData(container, 'data-todo-input', 'reminderAt').value = '2000-01-01T09:00'; + byData(container, 'data-todo-input', 'dueAt').value = '01/02/2000'; + const reminderDate = byData(container, 'data-todo-input', 'reminderDate'); + const reminderTime = byData(container, 'data-todo-input', 'reminderTime'); + if (!reminderDate || reminderDate.getAttribute('type') !== 'date') throw new Error('Todo reminder date input was not rendered'); + if (!reminderTime || reminderTime.getAttribute('type') !== 'time') throw new Error('Todo reminder time input was not rendered'); + reminderDate.value = '01/02/2000'; + reminderTime.value = '09:00'; byData(container, 'data-todo-action', 'save').click(); await flush(); @@ -241,7 +246,7 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make const createdTodo = storedAfterCreate[0]; if (createdTodo.workspaceRootPath !== 'Project') throw new Error('workspace Todo did not keep the Project root path'); if (createdTodo.status !== 'open' || createdTodo.priority !== 'high') throw new Error('Todo status or priority was not stored'); - if (createdTodo.dueAt !== '2000-01-01' || createdTodo.reminderAt !== '2000-01-01T09:00') throw new Error('Todo due/reminder metadata was not stored'); + if (createdTodo.dueAt !== '2000-01-02' || createdTodo.reminderAt !== '2000-01-02T09:00') throw new Error('Todo due/reminder metadata was not stored'); if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered'); const scheduledAfterCreate = apiState.notificationCalls.at(-1) || []; if (scheduledAfterCreate.length !== 1