feat(todo): To-Do in the look of the packing list, detail pane always in reach

Rows get the packing list's filled checkbox, priority, due date and list
as quiet badges, and the assignee as an avatar with the name as its
tooltip. The tasks sit in a card whose head names the filter and holds
the sort, Priority or Due date, which moved out of the sidebar; a second
click on the active one goes back to the manual order. The progress card
matches the packing list's.

The detail pane is a card with a box in its head that ticks the task
off, and Delete and Save changes stay in sight: the To-Do view now fills
the space under the Lists bar instead of a fixed height that pushed the
pane's foot off screen. The detail pane and the new-task form share one
set of fields, and a new list is named in a dialog, from the sidebar
and from the form; the dialog moved to shared/NameDialog for both lists.

The To-Do help pictures are re-rendered, the texts that described the
old sidebar and form follow in all 27 languages, and the wiki page is
updated. The phone screens are unchanged.
This commit is contained in:
Maurice
2026-09-25 13:29:06 +02:00
parent dc60f39554
commit 1f79776070
51 changed files with 654 additions and 713 deletions
+2 -1
View File
@@ -686,7 +686,8 @@ const SCRIPTS: Record<string, GuideScript> = {
target: p => todoPane(p).getByRole('button', { name: 'Save changes' }),
act: async p => {
await todoPane(p).getByRole('button', { name: 'Save changes' }).click()
await expect(task(p, EDITED_TASK)).toContainText(ASSIGNEE, { timeout: 20_000 })
// The row names its assignee as an avatar, labelled with the name.
await expect(task(p, EDITED_TASK).getByLabel(ASSIGNEE)).toBeVisible({ timeout: 20_000 })
await settle(p)
},
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -2,7 +2,7 @@ import {
CheckCheck, Luggage, Package, FolderPlus, Download, Trash2,
} from 'lucide-react'
import type { PackingState } from './usePackingListPanel'
import PackingNameDialog from './PackingNameDialog'
import NameDialog from '../shared/NameDialog'
export function PackingHeader(S: PackingState) {
const {
@@ -199,7 +199,7 @@ export function PackingHeader(S: PackingState) {
{/* Naming a new list or a template happens in a dialog of its own, not in a
field pushed into the page. */}
<PackingNameDialog
<NameDialog
open={canEdit && addingCategory}
title={t('packing.addCategory')}
placeholder={t('packing.newCategoryPlaceholder')}
@@ -209,7 +209,7 @@ export function PackingHeader(S: PackingState) {
onConfirm={handleAddNewCategory}
onClose={() => { setAddingCategory(false); setNewCatName('') }}
/>
<PackingNameDialog
<NameDialog
open={canEdit && isAdmin && items.length > 0 && showSaveTemplate}
title={t('packing.saveAsTemplate')}
placeholder={t('packing.templateName')}
@@ -502,7 +502,7 @@ describe('TodoListPanel — sidebar', () => {
const done = screen.getByRole('button', { name: /^Done/ });
fireEvent.mouseEnter(done);
expect(done.style.background).toBe('var(--bg-hover)');
expect(done.style.background).toBe('var(--bg-secondary)');
fireEvent.mouseLeave(done);
expect(done.style.background).toBe('transparent');
});
@@ -513,23 +513,27 @@ describe('TodoListPanel — sidebar', () => {
fireEvent.mouseLeave(all);
// 'all' is the active filter, so the hover handlers must leave it alone.
expect(all.style.background).toBe('var(--bg-hover)');
expect(all.style.background).toBe('var(--bg-tertiary)');
});
it('FE-COMP-TODO-032: hovering the priority sort toggles its background only while off', async () => {
it('FE-COMP-TODO-032: the sort track in the list head switches between priority, due date and the manual order', async () => {
const user = userEvent.setup();
render(<TodoListPanel tripId={1} items={[]} />);
const sort = screen.getByRole('button', { name: 'Priority' });
const prio = screen.getByRole('button', { name: 'Priority' });
const due = screen.getByRole('button', { name: 'Due date' });
fireEvent.mouseEnter(sort);
expect(sort.style.background).toBe('var(--bg-hover)');
fireEvent.mouseLeave(sort);
expect(sort.style.background).toBe('transparent');
await user.click(prio);
expect(prio).toHaveAttribute('aria-pressed', 'true');
expect(prio.style.background).toBe('var(--text-primary)');
await user.click(sort);
fireEvent.mouseEnter(sort);
// Active sort keeps its amber tint instead of the neutral hover.
expect(sort.style.background).toBe('rgba(245, 158, 11, 0.07)');
await user.click(due);
expect(prio).toHaveAttribute('aria-pressed', 'false');
expect(due).toHaveAttribute('aria-pressed', 'true');
// A second click on the active one goes back to the manual order.
await user.click(due);
expect(due).toHaveAttribute('aria-pressed', 'false');
expect(due.style.background).toBe('transparent');
});
it('FE-COMP-TODO-033: Escape abandons the new-list input', async () => {
@@ -554,7 +558,7 @@ describe('TodoListPanel — sidebar', () => {
await user.click(screen.getByRole('button', { name: 'Add list' }));
const input = await screen.findByPlaceholderText('List name');
await user.type(input, 'Errands');
fireEvent.click(input.nextElementSibling as HTMLElement);
fireEvent.click(screen.getByRole('button', { name: 'Add' }));
expect(posted).toBe(false);
expect(screen.queryByPlaceholderText('List name')).not.toBeInTheDocument();
@@ -687,11 +691,11 @@ describe('TodoListPanel — detail pane', () => {
);
await openDetail(user);
await user.click(screen.getByRole('button', { name: 'List name' }));
await user.click(within(screen.getByRole('region', { name: 'Task' })).getByRole('button', { name: 'Add list' }));
await user.type(await screen.findByPlaceholderText('List name'), 'Logistics');
await user.keyboard('{Enter}');
// Enter closes the inline editor and the typed list is offered as a new option.
// Enter confirms the name dialog and the typed list is offered as a new option.
expect(screen.queryByPlaceholderText('List name')).not.toBeInTheDocument();
expect(screen.getByText(/^Logistics/)).toBeInTheDocument();
@@ -705,7 +709,7 @@ describe('TodoListPanel — detail pane', () => {
withMembers();
await openDetail(user);
await user.click(screen.getByRole('button', { name: 'List name' }));
await user.click(within(screen.getByRole('region', { name: 'Task' })).getByRole('button', { name: 'Add list' }));
await user.type(await screen.findByPlaceholderText('List name'), 'Logistics');
await user.keyboard('{Escape}');
@@ -725,7 +729,7 @@ describe('TodoListPanel — detail pane', () => {
await user.click(screen.getByText('Plan route'));
await screen.findByText('Task');
const pane = screen.getByText('Task').closest('div[style*="border-left"]') as HTMLElement;
const pane = screen.getByRole('region', { name: 'Task' });
await user.click(within(pane).getByRole('button', { name: 'No list' }));
pickOption('Errands');
@@ -864,7 +868,7 @@ describe('TodoListPanel — new task pane', () => {
await user.type(screen.getByPlaceholderText('Task name'), 'Pack bags');
await user.type(screen.getByPlaceholderText('Description (optional)'), 'rain gear');
await user.click(screen.getByRole('button', { name: 'List name' }));
await user.click(within(screen.getByRole('region', { name: 'New task' })).getByRole('button', { name: 'Add list' }));
await user.type(await screen.findByPlaceholderText('List name'), 'Prep ');
await user.keyboard('{Enter}');
await user.click(screen.getByRole('button', { name: 'P1' }));
@@ -886,7 +890,7 @@ describe('TodoListPanel — new task pane', () => {
const user = userEvent.setup();
await openNew();
await user.click(screen.getByRole('button', { name: 'List name' }));
await user.click(within(screen.getByRole('region', { name: 'New task' })).getByRole('button', { name: 'Add list' }));
await user.type(await screen.findByPlaceholderText('List name'), 'Prep');
await user.keyboard('{Escape}');
@@ -999,7 +1003,7 @@ describe('TodoListPanel — mobile layout', () => {
await user.click(header);
expect(screen.getByText('Save changes')).toBeInTheDocument();
await user.click(header.parentElement!.querySelectorAll('button')[0]);
await user.click(within(screen.getByRole('region', { name: 'Task' })).getByRole('button', { name: 'Close' }));
await waitFor(() => expect(screen.queryByText('Save changes')).not.toBeInTheDocument());
});
@@ -1083,12 +1087,12 @@ describe('TodoListPanel — remaining paths', () => {
const all = screen.getByRole('button', { name: /^All/ });
fireEvent.mouseEnter(all);
expect(all.style.background).toBe('var(--bg-hover)');
expect(all.style.background).toBe('var(--bg-tertiary)');
const sort = screen.getByRole('button', { name: 'Priority' });
await user.click(sort);
fireEvent.mouseLeave(sort);
expect(sort.style.background).toBe('rgba(245, 158, 11, 0.07)');
expect(sort.style.background).toBe('var(--text-primary)');
});
it('FE-COMP-TODO-065: clearing the task name blocks the save', async () => {
@@ -1104,24 +1108,24 @@ describe('TodoListPanel — remaining paths', () => {
expect(put).toBe(false);
});
it('FE-COMP-TODO-066: the check button closes the inline list editor in both panes', async () => {
it('FE-COMP-TODO-066: the name dialog adds a new list in both panes', async () => {
const user = userEvent.setup();
const { rerender } = render(<TodoListPanel tripId={1} items={[buildTodoItem({ id: 40, name: 'Plan route', checked: 0 })]} addItemSignal={0} />);
await user.click(screen.getByText('Plan route'));
await user.click(await screen.findByRole('button', { name: 'List name' }));
await user.click(within(await screen.findByRole('region', { name: 'Task' })).getByRole('button', { name: 'Add list' }));
let input = await screen.findByPlaceholderText('List name');
await user.type(input, 'Logistics');
fireEvent.click(input.nextElementSibling as HTMLElement);
fireEvent.click(screen.getByRole('button', { name: 'Add' }));
expect(screen.queryByPlaceholderText('List name')).not.toBeInTheDocument();
expect(screen.getByText(/^Logistics/)).toBeInTheDocument();
rerender(<TodoListPanel tripId={1} items={[buildTodoItem({ id: 40, name: 'Plan route', checked: 0 })]} addItemSignal={1} />);
await screen.findByText('Create task');
await user.click(screen.getByRole('button', { name: 'List name' }));
await user.click(within(screen.getByRole('region', { name: 'New task' })).getByRole('button', { name: 'Add list' }));
input = await screen.findByPlaceholderText('List name');
await user.type(input, 'Prep');
fireEvent.click(input.nextElementSibling as HTMLElement);
fireEvent.click(screen.getByRole('button', { name: 'Add' }));
expect(screen.queryByPlaceholderText('List name')).not.toBeInTheDocument();
expect(screen.getByText(/^Prep/)).toBeInTheDocument();
});
@@ -1289,3 +1293,23 @@ describe('TodoListPanel — plugin contributions', () => {
expect(screen.getByText('Rainy')).toBeInTheDocument();
});
});
describe('TodoListPanel — detail pane head', () => {
it('FE-COMP-TODO-090: the box in the pane head ticks the task off', async () => {
const user = userEvent.setup();
let body: Record<string, unknown> | null = null;
server.use(http.put('/api/trips/1/todo/60', async ({ request }) => {
body = (await request.json()) as Record<string, unknown>;
return HttpResponse.json({ item: buildTodoItem({ id: 60, name: 'Buy SIM', checked: 1 }) });
}));
render(<TodoListPanel tripId={1} items={[buildTodoItem({ id: 60, name: 'Buy SIM', checked: 0 })]} />);
await user.click(screen.getByText('Buy SIM'));
const box = within(await screen.findByRole('region', { name: 'Task' })).getByRole('button', { name: 'Done' });
expect(box).toHaveAttribute('aria-pressed', 'false');
await user.click(box);
await waitFor(() => expect(body).toMatchObject({ checked: true }));
});
});
+214 -470
View File
@@ -1,33 +1,35 @@
import { Fragment, useState, useMemo, useEffect, useRef } from 'react'
import { avatarSrc } from '../../utils/avatarSrc'
import { Fragment, useState, useEffect, type CSSProperties } from 'react'
import { createPortal } from 'react-dom'
import { useTripStore } from '../../store/tripStore'
import { useCanDo } from '../../store/permissionsStore'
import { useToast } from '../shared/Toast'
import { useTranslation } from '../../i18n'
import { tripsApi } from '../../api/client'
import apiClient from '../../api/client'
import CustomSelect from '../shared/CustomSelect'
import { CustomDatePicker } from '../shared/CustomDateTimePicker'
import { formatDate as fmtDate } from '../../utils/formatters'
import {
CheckSquare, Square, Plus, ChevronRight, Flag,
X, Check, Calendar, User, FolderPlus, AlertCircle, ListChecks, Inbox, CheckCheck, Trash2,
} from 'lucide-react'
import { Check, Plus, Flag, X, Calendar, User, AlertCircle, Inbox, CheckCheck, Trash2 } from 'lucide-react'
import type { TodoItem } from '../../types'
import { KAT_COLORS, PRIO_CONFIG, katColor, type FilterType, type Member } from './todoListModel'
import { katColor, taskInputStyle, type FilterType, type Member, type TaskFieldValues } from './todoListModel'
import { useTodoList } from './useTodoList'
import TodoRow from './TodoRow'
import TodoTaskFields from './TodoTaskFields'
import { usePluginViewContributions, PluginCardFooter } from '../Plugins/PluginContributions'
import EmptyState from '../shared/EmptyState'
import NameDialog from '../shared/NameDialog'
/** The card the list and the detail pane sit in, the packing list's category card. */
const CARD: CSSProperties = {
background: 'var(--bg-card)', border: '1px solid var(--border-secondary)', borderRadius: 16,
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}
const HEAD_BAND: CSSProperties = {
display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px 10px 16px', background: 'var(--bg-tertiary)', flexShrink: 0,
}
// Sidebar filter row. Declared at module level so React keeps the same component
// type across renders — inline it and every re-render remounts the buttons,
// which drops clicks that are already in flight.
function SidebarItem({ id, icon: Icon, label, count, color, active, compact, onSelect }: {
id: string
icon: any
icon: React.ComponentType<{ size?: number; style?: CSSProperties }> | null
label: string
count: number
color?: string
@@ -40,23 +42,23 @@ function SidebarItem({ id, icon: Icon, label, count, color, active, compact, onS
title={compact ? label : undefined}
style={{
display: 'flex', alignItems: 'center', justifyContent: compact ? 'center' : 'flex-start',
gap: compact ? 0 : 8, width: '100%', padding: compact ? '8px 0' : '7px 12px',
border: 'none', borderRadius: 8, cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))',
background: active ? 'var(--bg-hover)' : 'transparent',
gap: compact ? 0 : 9, width: '100%', padding: compact ? '8px 0' : '8px 12px',
border: 'none', borderRadius: 10, cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))',
background: active ? 'var(--bg-tertiary)' : 'transparent',
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
fontWeight: active ? 600 : 400, transition: 'all 0.1s',
fontWeight: active ? 600 : 500, transition: 'all 0.1s',
position: 'relative',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--bg-hover)' }}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--bg-secondary)' }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent' }}>
{color ? (
<span style={{ width: compact ? 12 : 10, height: compact ? 12 : 10, borderRadius: '50%', background: color, flexShrink: 0 }} />
) : (
) : Icon ? (
<Icon size={compact ? 18 : 15} style={{ flexShrink: 0, opacity: 0.7 }} />
)}
) : null}
{!compact && <span style={{ flex: 1, textAlign: 'left' }}>{label}</span>}
{!compact && count > 0 && (
<span style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', background: 'var(--bg-hover)', borderRadius: 10, padding: '1px 7px', minWidth: 20, textAlign: 'center' }}>
<span style={{ fontSize: 'calc(10.5px * var(--fs-scale-caption, 1))', fontWeight: 700, color: 'var(--text-faint)', background: active ? 'var(--bg-card)' : 'var(--bg-tertiary)', borderRadius: 99, padding: '1px 7px', minWidth: 20, textAlign: 'center' }}>
{count}
</span>
)}
@@ -69,6 +71,14 @@ function SidebarItem({ id, icon: Icon, label, count, color, active, compact, onS
)
}
function SidebarCaption({ children }: { children: React.ReactNode }) {
return (
<div style={{ fontSize: 'calc(10.5px * var(--fs-scale-caption, 1))', fontWeight: 700, color: 'var(--text-faint)', padding: '14px 12px 4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{children}
</div>
)
}
export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tripId: number; items: TodoItem[]; addItemSignal?: number }) {
// Layout component: state/effects/derived/handlers live in useTodoList.
const {
@@ -110,6 +120,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
}
const selectFilter = (id: string) => setFilter(id as FilterType)
const pct = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0
// Filter title
const filterTitle = (() => {
@@ -120,125 +131,95 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
return filter
})()
// One segment of the sort track in the list's head band; a second click on the
// active one goes back to the manual order.
const sortSegment = (active: boolean): CSSProperties => ({
display: 'inline-flex', alignItems: 'center', gap: 5, height: 26, padding: isMobile ? '0 9px' : '0 11px', borderRadius: 99,
border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600,
background: active ? 'var(--text-primary)' : 'transparent', color: active ? 'var(--bg-primary)' : 'var(--text-muted)',
transition: 'all 0.12s',
})
return (
<div style={{ display: 'flex', height: 'calc(100vh - 180px)', minHeight: 400 }}>
<div style={{ display: 'flex', gap: 16, height: '100%', minHeight: 420 }}>
{/* ── Left Sidebar ── */}
<div style={{
width: isMobile ? 52 : 220, flexShrink: 0, borderRight: '1px solid var(--border-faint)',
padding: isMobile ? '12px 6px' : '16px 12px 16px 0', display: 'flex', flexDirection: 'column', gap: 2, overflowY: 'auto',
width: isMobile ? 52 : 220, flexShrink: 0,
display: 'flex', flexDirection: 'column', gap: 2, overflowY: 'auto',
transition: 'width 0.2s',
}}>
{/* Progress Card */}
{!isMobile && <div style={{
margin: '0 0 12px', padding: '14px 14px 12px', borderRadius: 14,
background: 'var(--bg-hover)',
border: '1px solid var(--border-primary)',
boxShadow: '0 1px 2px rgba(0,0,0,0.02)',
}}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginBottom: 8 }}>
<span style={{ fontSize: 'calc(18px * var(--fs-scale-subtitle, 1))', fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-0.02em' }}>
{totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0}%
</span>
{/* Progress card, the packing list's: done of all, the share, the bar */}
{!isMobile && (
<div style={{ margin: '0 0 8px', padding: '14px 16px', borderRadius: 16, background: 'var(--bg-card)', border: '1px solid var(--border-secondary)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ display: 'inline-flex', alignItems: 'baseline' }}>
<span style={{ fontSize: 'calc(22px * var(--fs-scale-title, 1))', fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{doneCount}</span>
<span style={{ fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 500, color: 'var(--text-faint)', lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>/{totalCount}</span>
</span>
<span style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, padding: '2px 7px', borderRadius: 99, background: 'var(--bg-tertiary)', color: 'var(--text-muted)', fontVariantNumeric: 'tabular-nums' }}>{pct}%</span>
</div>
<div style={{ height: 6, background: 'var(--bg-tertiary)', borderRadius: 99, overflow: 'hidden', marginTop: 12 }}>
<div style={{ height: '100%', width: `${pct}%`, background: 'var(--accent)', borderRadius: 99, transition: 'width 0.3s' }} />
</div>
<div style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', marginTop: 6 }}>
{doneCount} / {totalCount} {t('todo.completed')}
</div>
</div>
<div style={{ height: 4, background: 'var(--border-faint)', borderRadius: 2, overflow: 'hidden', marginBottom: 6 }}>
<div style={{ height: '100%', width: totalCount > 0 ? `${Math.round((doneCount / totalCount) * 100)}%` : '0%', background: '#22c55e', borderRadius: 2, transition: 'width 0.3s' }} />
</div>
<div style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)' }}>
{doneCount} / {totalCount} {t('todo.completed')}
</div>
</div>}
)}
{/* Smart filters */}
{!isMobile && <div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-faint)', padding: '8px 12px 4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{t('todo.sidebar.tasks')}
</div>}
{!isMobile && <SidebarCaption>{t('todo.sidebar.tasks')}</SidebarCaption>}
<SidebarItem id="all" icon={Inbox} label={t('todo.filter.all')} count={items.filter(i => !i.checked).length} active={filter === 'all'} compact={isMobile} onSelect={selectFilter} />
<SidebarItem id="my" icon={User} label={t('todo.filter.my')} count={myCount} active={filter === 'my'} compact={isMobile} onSelect={selectFilter} />
<SidebarItem id="overdue" icon={AlertCircle} label={t('todo.filter.overdue')} count={overdueCount} active={filter === 'overdue'} compact={isMobile} onSelect={selectFilter} />
<SidebarItem id="done" icon={CheckCheck} label={t('todo.filter.done')} count={doneCount} active={filter === 'done'} compact={isMobile} onSelect={selectFilter} />
{/* Sort by */}
{!isMobile && <div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-faint)', padding: '16px 12px 4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{t('todo.sidebar.sortBy')}
</div>}
<button type="button" onClick={() => { setSortByDue(false); setSortByPrio(v => !v) }}
title={isMobile ? t('todo.priority') : undefined}
style={{
display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : 'flex-start',
gap: isMobile ? 0 : 8, width: '100%', padding: isMobile ? '8px 0' : '7px 12px',
border: 'none', borderRadius: 8, cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))',
background: sortByPrio ? '#f59e0b12' : 'transparent',
color: sortByPrio ? '#f59e0b' : 'var(--text-secondary)',
fontWeight: sortByPrio ? 600 : 400, transition: 'all 0.1s',
}}
onMouseEnter={e => { if (!sortByPrio) e.currentTarget.style.background = 'var(--bg-hover)' }}
onMouseLeave={e => { if (!sortByPrio) e.currentTarget.style.background = 'transparent' }}>
<Flag size={isMobile ? 18 : 15} style={{ flexShrink: 0, opacity: 0.7 }} />
{!isMobile && <span style={{ flex: 1, textAlign: 'left' }}>{t('todo.priority')}</span>}
</button>
<button type="button" onClick={() => { setSortByPrio(false); setSortByDue(v => !v) }}
title={isMobile ? t('todo.detail.dueDate') : undefined}
style={{
display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : 'flex-start',
gap: isMobile ? 0 : 8, width: '100%', padding: isMobile ? '8px 0' : '7px 12px',
border: 'none', borderRadius: 8, cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))',
background: sortByDue ? '#f59e0b12' : 'transparent',
color: sortByDue ? '#f59e0b' : 'var(--text-secondary)',
fontWeight: sortByDue ? 600 : 400, transition: 'all 0.1s',
}}
onMouseEnter={e => { if (!sortByDue) e.currentTarget.style.background = 'var(--bg-hover)' }}
onMouseLeave={e => { if (!sortByDue) e.currentTarget.style.background = 'transparent' }}>
<Calendar size={isMobile ? 18 : 15} style={{ flexShrink: 0, opacity: 0.7 }} />
{!isMobile && <span style={{ flex: 1, textAlign: 'left' }}>{t('todo.detail.dueDate')}</span>}
</button>
{/* Categories */}
{!isMobile && <div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-faint)', padding: '16px 12px 4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{t('todo.sidebar.categories')}
</div>}
{/* Lists */}
{!isMobile && <SidebarCaption>{t('todo.sidebar.categories')}</SidebarCaption>}
{isMobile && <div style={{ height: 1, background: 'var(--border-faint)', margin: '8px 4px' }} />}
{categories.map(cat => (
<SidebarItem key={cat} id={cat} icon={null} label={cat} count={catCount(cat)} color={katColor(cat, categories)} active={filter === cat} compact={isMobile} onSelect={selectFilter} />
))}
{canEdit && (
addingCategory && !isMobile ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px' }}>
<input autoFocus value={newCategoryName} onChange={e => setNewCategoryName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') addCategory(); if (e.key === 'Escape') { setAddingCategory(false); setNewCategoryName('') } }}
placeholder={t('todo.newCategory')}
style={{ flex: 1, fontSize: 'calc(12px * var(--fs-scale-body, 1))', padding: '4px 6px', border: '1px solid var(--border-primary)', borderRadius: 5, background: 'var(--bg-hover)', color: 'var(--text-primary)', fontFamily: 'inherit', minWidth: 0 }} />
<button type="button" onClick={addCategory} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#22c55e', padding: 2 }}><Check size={13} /></button>
</div>
) : (
<button type="button" onClick={() => setAddingCategory(true)}
title={isMobile ? t('todo.addCategory') : undefined}
style={{ display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : 'flex-start', gap: isMobile ? 0 : 6, padding: isMobile ? '8px 0' : '7px 12px', fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: 'var(--text-faint)', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', width: '100%', textAlign: 'left' }}>
<Plus size={isMobile ? 18 : 13} /> {!isMobile && t('todo.addCategory')}
</button>
)
<button type="button" onClick={() => setAddingCategory(true)}
title={isMobile ? t('todo.addCategory') : undefined}
style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: isMobile ? 0 : 6,
margin: '6px 0 0', padding: isMobile ? '8px 0' : '8px 12px', borderRadius: 99, border: '1.5px dashed var(--border-primary)',
fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, color: 'var(--text-faint)', background: 'none', cursor: 'pointer', fontFamily: 'inherit', width: '100%',
}}>
<Plus size={isMobile ? 16 : 13} /> {!isMobile && t('todo.addCategory')}
</button>
)}
</div>
{/* ── Middle: Task List ── */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{/* Header */}
<div style={{ padding: '16px 20px 12px', borderBottom: '1px solid var(--border-faint)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<h2 style={{ margin: 0, fontSize: 'calc(22px * var(--fs-scale-title, 1))', fontWeight: 700, color: 'var(--text-primary)', letterSpacing: '-0.02em' }}>
{filterTitle}
</h2>
<span style={{ fontSize: 'calc(13px * var(--fs-scale-body, 1))', color: 'var(--text-faint)', background: 'var(--bg-hover)', borderRadius: 6, padding: '2px 8px', fontWeight: 600 }}>
{filtered.length}
</span>
{/* ── Middle: the task list, as a card ── */}
<div style={{ ...CARD, flex: 1, minWidth: 0 }}>
<div style={HEAD_BAND}>
<h2 style={{ margin: 0, fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 700, color: 'var(--text-primary)', letterSpacing: '-0.01em', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{filterTitle}
</h2>
<span style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-muted)', background: 'var(--bg-card)', borderRadius: 99, padding: '2px 8px', fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>
{filtered.length}
</span>
<span style={{ flex: 1 }} />
{/* Sort order: priority or due date, neither means the manual order */}
<div role="group" aria-label={t('todo.sidebar.sortBy')} style={{ display: 'inline-flex', alignItems: 'center', gap: 2, padding: 3, borderRadius: 99, background: 'var(--bg-card)', flexShrink: 0 }}>
<button type="button" aria-pressed={sortByPrio} title={t('todo.priority')} onClick={() => { setSortByDue(false); setSortByPrio(v => !v) }} style={sortSegment(sortByPrio)}>
<Flag size={12} />{!isMobile && t('todo.priority')}
</button>
<button type="button" aria-pressed={sortByDue} title={t('todo.detail.dueDate')} onClick={() => { setSortByPrio(false); setSortByDue(v => !v) }} style={sortSegment(sortByDue)}>
<Calendar size={12} />{!isMobile && t('todo.detail.dueDate')}
</button>
</div>
</div>
{/* Task list */}
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 0' }}>
<div style={{ flex: 1, overflowY: 'auto' }}>
{filtered.length === 0 ? <EmptyState scene="tasks" title={t('todo.empty')} /> : (
filtered.map(item => {
filtered.map((item, index) => {
const contributions = contribFor(item.id)
return (
<Fragment key={item.id}>
@@ -250,6 +231,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
isSelected={selectedId === item.id}
canEdit={canEdit}
formatDate={formatDate}
divider={index > 0}
onSelect={(id) => { setSelectedId(id); setIsAddingNew(false) }}
onToggle={(id, checked) => toggleTodoItem(tripId, id, checked)}
drag={canReorder ? {
@@ -262,7 +244,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
} : undefined}
/>
{contributions.length > 0 && (
<div style={{ padding: '0 20px 8px' }}><PluginCardFooter items={contributions} tripId={tripId} /></div>
<div style={{ padding: '0 16px 8px' }}><PluginCardFooter items={contributions} tripId={tripId} /></div>
)}
</Fragment>
)
@@ -273,54 +255,27 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
{/* ── Right: Detail Pane ── */}
{selectedItem && !isAddingNew && !isMobile && (
<DetailPane
item={selectedItem}
tripId={tripId}
categories={categories}
members={members}
onClose={() => setSelectedId(null)}
/>
<DetailPane item={selectedItem} tripId={tripId} categories={categories} members={members} onClose={() => setSelectedId(null)} />
)}
{selectedItem && !isAddingNew && isMobile && (
<div role="presentation" onClick={e => { if (e.target === e.currentTarget) setSelectedId(null) }}
style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end', paddingBottom: 'var(--bottom-nav-h)' }}>
<div style={{ width: '100%', maxHeight: '85vh', borderRadius: '16px 16px 0 0', overflow: 'auto' }}
ref={el => { if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px 16px 0 0' } } }}>
<DetailPane
item={selectedItem}
tripId={tripId}
categories={categories}
members={members}
onClose={() => setSelectedId(null)}
/>
<div style={{ width: '100%', maxHeight: '85vh', display: 'flex' }}>
<DetailPane variant="sheet" item={selectedItem} tripId={tripId} categories={categories} members={members} onClose={() => setSelectedId(null)} />
</div>
</div>
)}
{isAddingNew && !selectedItem && !isMobile && createPortal(
{isAddingNew && !selectedItem && createPortal(
<div role="presentation" onClick={e => { if (e.target === e.currentTarget) setIsAddingNew(false) }}
className="trek-modal-backdrop"
style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15,23,42,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: 'calc(var(--nav-h) + 60px)', paddingBottom: 40 }}>
<div style={{ width: 'min(520px, 92vw)', maxHeight: 'calc(100vh - var(--nav-h) - 120px)', overflow: 'auto', borderRadius: 16, boxShadow: '0 20px 60px rgba(0,0,0,0.25)' }}
ref={el => { if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px' } } }}>
<NewTaskPane
tripId={tripId}
categories={categories}
members={members}
defaultCategory={typeof filter === 'string' && categories.includes(filter) ? filter : null}
onCreated={(id) => { setIsAddingNew(false); setSelectedId(id) }}
onClose={() => setIsAddingNew(false)}
/>
</div>
</div>,
document.body
)}
{isAddingNew && !selectedItem && isMobile && createPortal(
<div role="presentation" onClick={e => { if (e.target === e.currentTarget) setIsAddingNew(false) }}
className="trek-modal-backdrop"
style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end', paddingBottom: 'var(--bottom-nav-h)' }}>
<div style={{ width: '100%', maxHeight: '85vh', borderRadius: '16px 16px 0 0', overflow: 'auto' }}
ref={el => { if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px 16px 0 0' } } }}>
style={isMobile
? { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end', paddingBottom: 'var(--bottom-nav-h)' }
: { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15,23,42,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: 'calc(var(--nav-h) + 60px)', paddingBottom: 40 }}>
<div style={isMobile
? { width: '100%', maxHeight: '85vh', display: 'flex' }
: { width: 'min(520px, 92vw)', maxHeight: 'calc(100vh - var(--nav-h) - 120px)', display: 'flex', boxShadow: '0 20px 60px rgba(0,0,0,0.25)', borderRadius: 16 }}>
<NewTaskPane
variant={isMobile ? 'sheet' : 'dialog'}
tripId={tripId}
categories={categories}
members={members}
@@ -332,17 +287,69 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
</div>,
document.body
)}
<NameDialog
open={canEdit && addingCategory}
title={t('todo.addCategory')}
placeholder={t('todo.newCategory')}
confirmLabel={t('common.add')}
value={newCategoryName}
onChange={setNewCategoryName}
onConfirm={addCategory}
onClose={() => { setAddingCategory(false); setNewCategoryName('') }}
/>
</div>
)
}
type PaneVariant = 'side' | 'dialog' | 'sheet'
/** The pane's frame: a card beside the list, a dialog, or a sheet from the bottom on a phone. */
function paneFrame(variant: PaneVariant): CSSProperties {
if (variant === 'side') return { ...CARD, width: 340, flexShrink: 0 }
return { ...CARD, width: '100%', border: 'none', borderRadius: variant === 'sheet' ? '16px 16px 0 0' : 16 }
}
function PaneHead({ title, onClose, lead }: { title: string; onClose: () => void; lead?: React.ReactNode }) {
const { t } = useTranslation()
return (
<div style={HEAD_BAND}>
{lead}
<span style={{ flex: 1, fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 700, color: 'var(--text-primary)' }}>{title}</span>
<button type="button" onClick={onClose} aria-label={t('common.close')} style={{ width: 28, height: 28, borderRadius: 8, background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'grid', placeItems: 'center' }}>
<X size={16} />
</button>
</div>
)
}
const nameFieldStyle: CSSProperties = {
width: '100%', fontSize: 'calc(16px * var(--fs-scale-subtitle, 1))', fontWeight: 600, border: 'none', padding: '2px 0',
background: 'transparent', color: 'var(--text-primary)', outline: 'none', fontFamily: 'inherit',
}
const FORM_BODY: CSSProperties = { flex: 1, minHeight: 0, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: 16 }
const FOOTER: CSSProperties = { padding: '12px 16px', borderTop: '1px solid var(--border-faint)', display: 'flex', gap: 8, flexShrink: 0, background: 'var(--bg-card)' }
const primaryButton = (enabled: boolean): CSSProperties => ({
flex: 1, padding: '9px 16px', borderRadius: 10, fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontWeight: 600,
cursor: enabled ? 'pointer' : 'default', fontFamily: 'inherit', border: 'none',
background: 'var(--accent)', color: 'var(--accent-text)', opacity: enabled ? 1 : 0.4, transition: 'opacity 0.15s',
})
// ── Detail Pane (right side) ──────────────────────────────────────────────
function DetailPane({ item, tripId, categories, members, onClose }: {
/** A task's editable fields as the form holds them: empty strings where the task has nothing. */
function fieldsOf(item: TodoItem): TaskFieldValues {
return {
desc: item.description || '', priority: item.priority || 0, category: item.category || '',
dueDate: item.due_date || '', assignedUserId: item.assigned_user_id,
}
}
function DetailPane({ item, tripId, categories, members, onClose, variant = 'side' }: {
item: TodoItem; tripId: number; categories: string[]; members: Member[];
onClose: () => void;
onClose: () => void; variant?: PaneVariant;
}) {
const { updateTodoItem, deleteTodoItem } = useTripStore()
const { updateTodoItem, deleteTodoItem, toggleTodoItem } = useTripStore()
const trip = useTripStore((s) => s.trip)
const can = useCanDo()
const canEdit = can('packing_edit', trip)
@@ -350,36 +357,30 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
const { t } = useTranslation()
const [name, setName] = useState(item.name)
const [desc, setDesc] = useState(item.description || '')
const [dueDate, setDueDate] = useState(item.due_date || '')
const [category, setCategory] = useState(item.category || '')
const [addingCategory, setAddingCategoryInline] = useState(false)
const [assignedUserId, setAssignedUserId] = useState<number | null>(item.assigned_user_id)
const [priority, setPriority] = useState(item.priority || 0)
const [fields, setFields] = useState<TaskFieldValues>(() => fieldsOf(item))
const [saving, setSaving] = useState(false)
// Sync when selected item changes
useEffect(() => {
setName(item.name)
setDesc(item.description || '')
setDueDate(item.due_date || '')
setCategory(item.category || '')
setAssignedUserId(item.assigned_user_id)
setPriority(item.priority || 0)
setFields({
desc: item.description || '', priority: item.priority || 0, category: item.category || '',
dueDate: item.due_date || '', assignedUserId: item.assigned_user_id,
})
}, [item.id, item.name, item.description, item.due_date, item.category, item.assigned_user_id, item.priority])
const hasChanges = name !== item.name || desc !== (item.description || '') ||
dueDate !== (item.due_date || '') || category !== (item.category || '') ||
assignedUserId !== item.assigned_user_id || priority !== (item.priority || 0)
const hasChanges = name !== item.name || fields.desc !== (item.description || '') ||
fields.dueDate !== (item.due_date || '') || fields.category !== (item.category || '') ||
fields.assignedUserId !== item.assigned_user_id || fields.priority !== (item.priority || 0)
const save = async () => {
if (!name.trim() || !hasChanges) return
setSaving(true)
try {
await updateTodoItem(tripId, item.id, {
name: name.trim(), description: desc || null,
due_date: dueDate || null, category: category || null,
assigned_user_id: assignedUserId, priority,
name: name.trim(), description: fields.desc || null,
due_date: fields.dueDate || null, category: fields.category || null,
assigned_user_id: fields.assignedUserId, priority: fields.priority,
})
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
setSaving(false)
@@ -392,172 +393,41 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
}
const labelClass = 'block text-xs font-medium text-content-secondary mb-1'
const inputStyle: React.CSSProperties = {
width: '100%', fontSize: 'calc(13px * var(--fs-scale-body, 1))', padding: '8px 10px', border: '1px solid var(--border-primary)',
borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit',
}
const done = !!item.checked
const doneBox = (
<button type="button" onClick={() => canEdit && toggleTodoItem(tripId, item.id, !done)} aria-pressed={done} aria-label={t('todo.filter.done')} title={t('todo.filter.done')}
style={{
flexShrink: 0, cursor: canEdit ? 'pointer' : 'default', padding: 0, width: 20, height: 20, borderRadius: 6, display: 'grid', placeItems: 'center',
border: `1.5px solid ${done ? 'var(--accent)' : 'var(--text-faint)'}`, background: done ? 'var(--accent)' : 'transparent', color: 'var(--accent-text)',
}}>
<Check size={12} strokeWidth={3} style={{ opacity: done ? 1 : 0 }} />
</button>
)
return (
<div style={{
width: 320, flexShrink: 0, borderLeft: '1px solid var(--border-faint)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-primary)',
}}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px 12px', borderBottom: '1px solid var(--border-faint)' }}>
<span style={{ fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 700, color: 'var(--text-primary)' }}>{t('todo.detail.title')}</span>
<button type="button" onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', padding: 4 }}>
<X size={16} />
</button>
<div role="region" aria-label={t('todo.detail.title')} style={paneFrame(variant)}>
<PaneHead title={t('todo.detail.title')} onClose={onClose} lead={doneBox} />
<div style={FORM_BODY}>
<input value={name} onChange={e => setName(e.target.value)} disabled={!canEdit}
style={{ ...nameFieldStyle, textDecoration: done ? 'line-through' : 'none', color: done ? 'var(--text-faint)' : 'var(--text-primary)' }}
placeholder={t('todo.namePlaceholder')} />
<TodoTaskFields values={fields} onChange={patch => setFields(f => ({ ...f, ...patch }))} categories={categories} members={members} canEdit={canEdit} />
</div>
{/* Form */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* Name */}
<div>
<input value={name} onChange={e => setName(e.target.value)} disabled={!canEdit}
style={{ ...inputStyle, fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 600, border: 'none', padding: '4px 0', background: 'transparent' }}
placeholder={t('todo.namePlaceholder')} />
</div>
{/* Description */}
<div>
<label className={labelClass}>{t('todo.detail.description')}</label>
<textarea value={desc} onChange={e => setDesc(e.target.value)} disabled={!canEdit} rows={4}
placeholder={t('todo.descriptionPlaceholder')}
style={{ ...inputStyle, resize: 'vertical', minHeight: 80 }} />
</div>
{/* Priority */}
<div>
<label className={labelClass}>{t('todo.detail.priority')}</label>
<div style={{ display: 'flex', gap: 4 }}>
{[0, 1, 2, 3].map(p => {
const cfg = PRIO_CONFIG[p]
const isActive = priority === p
return (
<button type="button" key={p} onClick={() => canEdit && setPriority(p)}
style={{
flex: 1, padding: '6px 0', borderRadius: 6, fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, cursor: canEdit ? 'pointer' : 'default',
fontFamily: 'inherit', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
border: `1px solid ${isActive && cfg ? cfg.color + '40' : 'var(--border-primary)'}`,
background: isActive && cfg ? cfg.color + '12' : 'transparent',
color: isActive && cfg ? cfg.color : isActive ? 'var(--text-primary)' : 'var(--text-faint)',
transition: 'all 0.1s',
}}>
{cfg ? <><Flag size={10} />{cfg.label}</> : t('todo.detail.noPriority')}
</button>
)
})}
</div>
</div>
{/* Category */}
<div>
<label className={labelClass}>{t('todo.detail.category')}</label>
{addingCategory ? (
<div style={{ display: 'flex', gap: 4 }}>
<input
autoFocus
value={category}
onChange={e => setCategory(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') setAddingCategoryInline(false); if (e.key === 'Escape') { setCategory(''); setAddingCategoryInline(false) } }}
placeholder={t('todo.newCategory')}
style={{ flex: 1, fontSize: 'calc(13px * var(--fs-scale-body, 1))', padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', outline: 'none' }}
/>
<button type="button" onClick={() => setAddingCategoryInline(false)}
style={{ background: 'var(--bg-hover)', border: '1px solid var(--border-primary)', borderRadius: 8, padding: '0 10px', cursor: 'pointer', color: 'var(--text-primary)' }}>
<Check size={14} />
</button>
</div>
) : (
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<CustomSelect
value={category}
onChange={v => setCategory(String(v))}
options={[
{ value: '', label: t('todo.noCategory') },
...categories.map(c => ({
value: c, label: c,
icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: katColor(c, categories), display: 'inline-block' }} />,
})),
...(category && !categories.includes(category) ? [{
value: category, label: `${category} (${t('todo.newCategoryLabel') || 'new'})`,
icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#9ca3af', display: 'inline-block' }} />,
}] : []),
]}
placeholder={t('todo.noCategory')}
size="sm"
disabled={!canEdit}
/>
</div>
{canEdit && (
<button type="button" onClick={() => { setCategory(''); setAddingCategoryInline(true) }}
title={t('todo.newCategory')}
style={{ background: 'var(--bg-hover)', border: '1px solid var(--border-primary)', borderRadius: 8, padding: '0 10px', cursor: 'pointer', color: 'var(--text-muted)', fontFamily: 'inherit' }}>
<Plus size={14} />
</button>
)}
</div>
)}
</div>
{/* Due date */}
<div>
<label className={labelClass}>{t('todo.detail.dueDate')}</label>
<CustomDatePicker
value={dueDate}
onChange={v => setDueDate(v)}
/>
</div>
{/* Assigned to */}
<div>
<label className={labelClass}>{t('todo.detail.assignedTo')}</label>
<CustomSelect
value={String(assignedUserId ?? '')}
onChange={v => setAssignedUserId(v ? Number(v) : null)}
options={[
{ value: '', label: t('todo.unassigned'), icon: <User size={14} className="text-content-faint" /> },
...members.map(m => ({
value: String(m.id),
label: m.is_guest ? `${m.username} · ${t('members.guest')}` : m.username,
icon: m.avatar ? (
<img src={avatarSrc(m.avatar)!} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
) : (
<span style={{ width: 18, height: 18, borderRadius: '50%', background: 'var(--border-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontWeight: 600 }}>
{m.username.charAt(0).toUpperCase()}
</span>
),
})),
]}
placeholder={t('todo.unassigned')}
size="sm"
disabled={!canEdit}
/>
</div>
</div>
{/* Footer actions */}
{/* Always in sight: the pane scrolls between its head and this foot. */}
{canEdit && (
<div style={{ padding: '12px 20px', borderTop: '1px solid var(--border-faint)', display: 'flex', gap: 8 }}>
<div style={FOOTER}>
<button type="button" onClick={handleDelete}
style={{
flex: 1, padding: '9px 16px', borderRadius: 8, fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
border: '1px solid var(--border-primary)', background: 'transparent', color: 'var(--text-secondary)',
padding: '9px 14px', borderRadius: 10, fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
border: 'none', background: 'rgba(239,68,68,0.1)', color: '#ef4444',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
}}>
<Trash2 size={13} />
{t('todo.detail.delete')}
</button>
<button type="button" onClick={save} disabled={!hasChanges || saving}
style={{
flex: 1, padding: '9px 16px', borderRadius: 8, fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: hasChanges ? 'pointer' : 'default', fontFamily: 'inherit',
border: 'none', background: hasChanges ? 'var(--text-primary)' : 'var(--border-faint)',
color: hasChanges ? 'var(--bg-primary)' : 'var(--text-faint)',
transition: 'all 0.15s',
}}>
<button type="button" onClick={save} disabled={!hasChanges || saving} style={primaryButton(hasChanges && !saving)}>
{saving ? '...' : t('todo.detail.save')}
</button>
</div>
@@ -566,36 +436,29 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
)
}
// ── New Task Pane (right side, for creating) ──────────────────────────────
// ── New Task Pane (a dialog on a wide screen, a sheet on a phone) ─────────
function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated, onClose }: {
function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated, onClose, variant = 'dialog' }: {
tripId: number; categories: string[]; members: Member[]; defaultCategory: string | null;
onCreated: (id: number) => void; onClose: () => void;
onCreated: (id: number) => void; onClose: () => void; variant?: PaneVariant;
}) {
const { addTodoItem } = useTripStore()
const toast = useToast()
const { t } = useTranslation()
const [name, setName] = useState('')
const [desc, setDesc] = useState('')
const [dueDate, setDueDate] = useState('')
const [category, setCategory] = useState(defaultCategory || '')
const [addingCategory, setAddingCategoryInline] = useState(false)
const [assignedUserId, setAssignedUserId] = useState<number | null>(null)
const [priority, setPriority] = useState(0)
const [fields, setFields] = useState<TaskFieldValues>({ desc: '', priority: 0, category: defaultCategory || '', dueDate: '', assignedUserId: null })
const [saving, setSaving] = useState(false)
const labelClass = 'block text-xs font-medium text-content-secondary mb-1'
const create = async () => {
if (!name.trim()) return
setSaving(true)
try {
const trimmedCategory = category.trim()
const trimmedCategory = fields.category.trim()
const item = await addTodoItem(tripId, {
name: name.trim(), description: desc || null, priority,
due_date: dueDate || null, category: trimmedCategory || null,
assigned_user_id: assignedUserId,
name: name.trim(), description: fields.desc || null, priority: fields.priority,
due_date: fields.dueDate || null, category: trimmedCategory || null,
assigned_user_id: fields.assignedUserId,
})
if (item?.id) onCreated(item.id)
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
@@ -603,138 +466,19 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
}
return (
<div style={{
width: 320, flexShrink: 0, borderLeft: '1px solid var(--border-faint)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-primary)',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px 12px', borderBottom: '1px solid var(--border-faint)' }}>
<span style={{ fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 700, color: 'var(--text-primary)' }}>{t('todo.newItem')}</span>
<button type="button" onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', padding: 4 }}>
<X size={16} />
</button>
<div role="region" aria-label={t('todo.newItem')} style={paneFrame(variant)}>
<PaneHead title={t('todo.newItem')} onClose={onClose} />
<div style={FORM_BODY}>
<input autoFocus value={name} onChange={e => setName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && name.trim()) create() }}
style={{ ...taskInputStyle, fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 600 }}
placeholder={t('todo.namePlaceholder')} />
<TodoTaskFields values={fields} onChange={patch => setFields(f => ({ ...f, ...patch }))} categories={categories} members={members} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<input autoFocus value={name} onChange={e => setName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && name.trim()) create() }}
style={{ width: '100%', fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 600, border: 'none', padding: '4px 0', background: 'transparent', color: 'var(--text-primary)', outline: 'none', fontFamily: 'inherit' }}
placeholder={t('todo.namePlaceholder')} />
</div>
<div>
<label className={labelClass}>{t('todo.detail.description')}</label>
<textarea value={desc} onChange={e => setDesc(e.target.value)} rows={4}
placeholder={t('todo.descriptionPlaceholder')}
style={{ width: '100%', fontSize: 'calc(13px * var(--fs-scale-body, 1))', padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', resize: 'vertical', minHeight: 80 }} />
</div>
<div>
<label className={labelClass}>{t('todo.detail.category')}</label>
{addingCategory ? (
<div style={{ display: 'flex', gap: 4 }}>
<input
autoFocus
value={category}
onChange={e => setCategory(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') setAddingCategoryInline(false); if (e.key === 'Escape') { setCategory(''); setAddingCategoryInline(false) } }}
placeholder={t('todo.newCategory')}
style={{ flex: 1, fontSize: 'calc(13px * var(--fs-scale-body, 1))', padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', outline: 'none' }}
/>
<button type="button" onClick={() => setAddingCategoryInline(false)}
style={{ background: 'var(--bg-hover)', border: '1px solid var(--border-primary)', borderRadius: 8, padding: '0 10px', cursor: 'pointer', color: 'var(--text-primary)' }}>
<Check size={14} />
</button>
</div>
) : (
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<CustomSelect
value={category}
onChange={v => setCategory(String(v))}
options={[
{ value: '', label: t('todo.noCategory') },
...categories.map(c => ({
value: c, label: c,
icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: katColor(c, categories), display: 'inline-block' }} />,
})),
...(category && !categories.includes(category) ? [{
value: category, label: `${category} (${t('todo.newCategoryLabel') || 'new'})`,
icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#9ca3af', display: 'inline-block' }} />,
}] : []),
]}
placeholder={t('todo.noCategory')}
size="sm"
/>
</div>
<button type="button" onClick={() => { setCategory(''); setAddingCategoryInline(true) }}
title={t('todo.newCategory')}
style={{ background: 'var(--bg-hover)', border: '1px solid var(--border-primary)', borderRadius: 8, padding: '0 10px', cursor: 'pointer', color: 'var(--text-muted)', fontFamily: 'inherit' }}>
<Plus size={14} />
</button>
</div>
)}
</div>
<div>
<label className={labelClass}>{t('todo.detail.priority')}</label>
<div style={{ display: 'flex', gap: 4 }}>
{[0, 1, 2, 3].map(p => {
const cfg = PRIO_CONFIG[p]
const isActive = priority === p
return (
<button type="button" key={p} onClick={() => setPriority(p)}
style={{
flex: 1, padding: '6px 0', borderRadius: 6, fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, cursor: 'pointer',
fontFamily: 'inherit', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
border: `1px solid ${isActive && cfg ? cfg.color + '40' : 'var(--border-primary)'}`,
background: isActive && cfg ? cfg.color + '12' : 'transparent',
color: isActive && cfg ? cfg.color : isActive ? 'var(--text-primary)' : 'var(--text-faint)',
transition: 'all 0.1s',
}}>
{cfg ? <><Flag size={10} />{cfg.label}</> : t('todo.detail.noPriority')}
</button>
)
})}
</div>
</div>
<div>
<label className={labelClass}>{t('todo.detail.dueDate')}</label>
<CustomDatePicker value={dueDate} onChange={v => setDueDate(v)} />
</div>
<div>
<label className={labelClass}>{t('todo.detail.assignedTo')}</label>
<CustomSelect
value={String(assignedUserId ?? '')}
onChange={v => setAssignedUserId(v ? Number(v) : null)}
options={[
{ value: '', label: t('todo.unassigned'), icon: <User size={14} className="text-content-faint" /> },
...members.map(m => ({
value: String(m.id), label: m.username,
icon: m.avatar ? (
<img src={avatarSrc(m.avatar)!} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
) : (
<span style={{ width: 18, height: 18, borderRadius: '50%', background: 'var(--border-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontWeight: 600 }}>
{m.username.charAt(0).toUpperCase()}
</span>
),
})),
]}
placeholder={t('todo.unassigned')}
size="sm"
/>
</div>
</div>
<div style={{ padding: '12px 20px', borderTop: '1px solid var(--border-faint)' }}>
<button type="button" onClick={create} disabled={!name.trim() || saving}
style={{
width: '100%', padding: '9px 16px', borderRadius: 8, fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: name.trim() ? 'pointer' : 'default', fontFamily: 'inherit',
border: 'none', background: name.trim() ? 'var(--text-primary)' : 'var(--border-faint)',
color: name.trim() ? 'var(--bg-primary)' : 'var(--text-faint)', transition: 'all 0.15s',
}}>
<div style={FOOTER}>
<button type="button" onClick={create} disabled={!name.trim() || saving} style={primaryButton(!!name.trim() && !saving)}>
{saving ? '...' : t('todo.detail.create')}
</button>
</div>
+2 -1
View File
@@ -143,7 +143,8 @@ describe('TodoRow', () => {
it('FE-W4TDR-014: renders the assignee avatar for a member with a picture', () => {
const { container } = setup(todo({ assigned_user_id: 1 } as Partial<TodoItem>))
expect(screen.getByText('ada')).toBeInTheDocument()
// The assignee is an avatar; the name is its tooltip and label.
expect(screen.getByTitle('ada')).toHaveAttribute('aria-label', 'ada')
expect(container.querySelector('img')).toHaveAttribute('src', '/uploads/avatars/ada.png')
})
+57 -57
View File
@@ -1,11 +1,19 @@
import { CheckSquare, Square, ChevronRight, Flag, Calendar, GripVertical, UserRound } from 'lucide-react'
import type { CSSProperties } from 'react'
import { Check, ChevronRight, Flag, Calendar, GripVertical, UserRound } from 'lucide-react'
import { avatarSrc } from '../../utils/avatarSrc'
import type { TodoItem } from '../../types'
import { katColor, PRIO_CONFIG, type Member } from './todoListModel'
/** A quiet pill for the row's facts: filled, no frame, the packing list's badge. */
const badge: CSSProperties = {
fontSize: 'calc(10.5px * var(--fs-scale-caption, 1))', display: 'inline-flex', alignItems: 'center', gap: 4,
padding: '2px 8px', borderRadius: 99, fontWeight: 600,
color: 'var(--text-muted)', background: 'var(--bg-tertiary)',
}
/** A single task row in the todo list. Pure presentation; all behaviour is
* delegated to onSelect/onToggle so TodoListPanel stays a layout component. */
export default function TodoRow({ item, members, categories, today, isSelected, canEdit, formatDate, onSelect, onToggle, drag }: {
export default function TodoRow({ item, members, categories, today, isSelected, canEdit, formatDate, onSelect, onToggle, divider = false, drag }: {
item: TodoItem
members: Member[]
categories: string[]
@@ -15,6 +23,8 @@ export default function TodoRow({ item, members, categories, today, isSelected,
formatDate: (d: string) => string
onSelect: (id: number | null) => void
onToggle: (id: number, checked: boolean) => void
/** Draws the hairline above the row; the list leaves it off its first row. */
divider?: boolean
// Drag-to-reorder (#969); only provided when manual ordering is active.
drag?: {
isDragging: boolean
@@ -30,6 +40,7 @@ export default function TodoRow({ item, members, categories, today, isSelected,
const isOverdue = item.due_date && !done && item.due_date < today
const catColor = item.category ? katColor(item.category, categories) : null
const canDrag = canEdit && !!drag
const prio = item.priority > 0 ? PRIO_CONFIG[item.priority] : undefined
return (
<div key={item.id}
@@ -53,14 +64,14 @@ export default function TodoRow({ item, members, categories, today, isSelected,
onDragLeave={canDrag ? (e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) drag!.onOver(-1) }) : undefined}
onDrop={canDrag ? (e => { e.preventDefault(); e.stopPropagation(); drag!.onDrop(item.id) }) : undefined}
style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '10px 20px',
borderBottom: '1px solid var(--border-faint)', cursor: 'pointer',
background: isSelected ? 'var(--bg-hover)' : 'transparent',
display: 'flex', alignItems: 'center', gap: 10, padding: '11px 14px', position: 'relative',
borderTop: divider ? '1px solid var(--border-faint)' : 'none', cursor: 'pointer',
background: isSelected ? 'var(--bg-secondary)' : 'transparent',
boxShadow: drag?.isOver ? 'inset 3px 0 0 0 var(--accent)' : isSelected ? 'inset 3px 0 0 0 var(--text-primary)' : 'none',
opacity: drag?.isDragging ? 0.4 : 1,
boxShadow: drag?.isOver ? 'inset 3px 0 0 0 var(--accent)' : 'none',
transition: 'background 0.1s, opacity 0.15s',
}}
onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = 'rgba(0,0,0,0.02)' }}
onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = 'var(--bg-secondary)' }}
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = 'transparent' }}>
{canDrag && (
@@ -72,23 +83,29 @@ export default function TodoRow({ item, members, categories, today, isSelected,
onClick={e => e.stopPropagation()}
onDragStart={e => { e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; drag!.onStart(item.id) }}
onDragEnd={() => drag!.onEnd()}
style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0, marginLeft: -6 }}
style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0, marginLeft: -4, opacity: 0.6 }}
>
<GripVertical size={14} />
<GripVertical size={13} />
</div>
)}
{/* Checkbox */}
{/* The packing list's box: filled with the accent once done, an outline until then. */}
<button type="button" onClick={e => { e.stopPropagation(); if (canEdit) onToggle(item.id, !done) }}
style={{ background: 'none', border: 'none', cursor: canEdit ? 'pointer' : 'default', padding: 0, flexShrink: 0,
color: done ? '#22c55e' : 'var(--border-primary)' }}>
{done ? <CheckSquare size={18} /> : <Square size={18} />}
aria-pressed={done}
style={{
flexShrink: 0, cursor: canEdit ? 'pointer' : 'default', padding: 0, width: 20, height: 20, borderRadius: 6,
display: 'grid', placeItems: 'center',
border: `1.5px solid ${done ? 'var(--accent)' : 'var(--text-faint)'}`,
background: done ? 'var(--accent)' : 'transparent', color: 'var(--accent-text)',
transition: 'background 180ms ease, border-color 180ms ease',
}}>
<Check size={12} strokeWidth={3} style={{ opacity: done ? 1 : 0, transform: done ? 'scale(1)' : 'scale(0.5)', transition: 'opacity 180ms ease, transform 200ms ease' }} />
</button>
{/* Content */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 'calc(14px * var(--fs-scale-body, 1))', color: done ? 'var(--text-faint)' : 'var(--text-primary)',
fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 500, color: done ? 'var(--text-faint)' : 'var(--text-primary)',
textDecoration: done ? 'line-through' : 'none', lineHeight: 1.4,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
@@ -100,66 +117,49 @@ export default function TodoRow({ item, members, categories, today, isSelected,
{item.description}
</div>
)}
{/* Inline badges */}
{!!(item.priority || item.due_date || catColor || assignedUser) && (
<div style={{ display: 'flex', gap: 5, marginTop: 5, flexWrap: 'wrap' }}>
{item.priority > 0 && PRIO_CONFIG[item.priority] && (
<span style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', display: 'inline-flex', alignItems: 'center', gap: 3,
padding: '2px 7px', borderRadius: 5, fontWeight: 600,
color: PRIO_CONFIG[item.priority].color,
background: `${PRIO_CONFIG[item.priority].color}10`,
border: `1px solid ${PRIO_CONFIG[item.priority].color}25`,
}}>
<Flag size={9} />{PRIO_CONFIG[item.priority].label}
{/* Priority, date and list as quiet badges */}
{!!(prio || item.due_date || catColor) && (
<div style={{ display: 'flex', gap: 5, marginTop: 6, flexWrap: 'wrap' }}>
{prio && (
<span style={{ ...badge, color: prio.color, background: `color-mix(in srgb, ${prio.color} 12%, transparent)` }}>
<Flag size={9} />{prio.label}
</span>
)}
{item.due_date && (
<span style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', display: 'inline-flex', alignItems: 'center', gap: 3,
padding: '2px 7px', borderRadius: 5, fontWeight: 500,
color: isOverdue ? '#ef4444' : 'var(--text-secondary)',
background: isOverdue ? 'rgba(239,68,68,0.08)' : 'var(--bg-hover)',
border: `1px solid ${isOverdue ? 'rgba(239,68,68,0.15)' : 'var(--border-faint)'}`,
...badge,
color: isOverdue ? '#ef4444' : 'var(--text-muted)',
background: isOverdue ? 'rgba(239,68,68,0.1)' : 'var(--bg-tertiary)',
}}>
<Calendar size={9} />{formatDate(item.due_date)}
</span>
)}
{catColor && (
<span style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', display: 'inline-flex', alignItems: 'center', gap: 4,
padding: '2px 7px', borderRadius: 5, fontWeight: 500,
color: 'var(--text-secondary)', background: 'var(--bg-hover)',
border: '1px solid var(--border-faint)',
}}>
<span style={badge}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: catColor, flexShrink: 0 }} />
{item.category}
</span>
)}
{assignedUser && (
<span style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', display: 'inline-flex', alignItems: 'center', gap: 4,
padding: '2px 7px', borderRadius: 5, fontWeight: 500,
color: 'var(--text-secondary)', background: 'var(--bg-hover)',
border: '1px solid var(--border-faint)',
}}>
{assignedUser.avatar ? (
<img src={avatarSrc(assignedUser.avatar)!} style={{ width: 13, height: 13, borderRadius: '50%', objectFit: 'cover' }} alt="" />
) : (
<span style={{ width: 13, height: 13, borderRadius: '50%', background: 'var(--border-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(7px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontWeight: 700 }}>
{assignedUser.username.charAt(0).toUpperCase()}
</span>
)}
{assignedUser.is_guest && <UserRound size={11} style={{ opacity: 0.7 }} />}
{assignedUser.username}
</span>
)}
</div>
)}
</div>
{/* Chevron */}
<ChevronRight size={16} color="var(--text-faint)" style={{ flexShrink: 0, opacity: 0.4 }} />
{/* Who does it, as an avatar: the name is its tooltip and label. */}
{assignedUser && (
<span title={assignedUser.username} aria-label={assignedUser.username}
style={{ display: 'inline-flex', alignItems: 'center', gap: 3, flexShrink: 0 }}>
{assignedUser.avatar ? (
<img src={avatarSrc(assignedUser.avatar)!} style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover' }} alt="" />
) : (
<span style={{ width: 22, height: 22, borderRadius: '50%', background: 'var(--bg-tertiary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-muted)', fontWeight: 700 }}>
{assignedUser.username.charAt(0).toUpperCase()}
</span>
)}
{assignedUser.is_guest && <UserRound size={11} style={{ color: 'var(--text-faint)' }} />}
</span>
)}
<ChevronRight size={15} color="var(--text-faint)" style={{ flexShrink: 0, opacity: isSelected ? 0.8 : 0.35 }} />
</div>
)
}
@@ -0,0 +1,145 @@
import { useState } from 'react'
import { Flag, Plus, User } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { avatarSrc } from '../../utils/avatarSrc'
import CustomSelect from '../shared/CustomSelect'
import { CustomDatePicker } from '../shared/CustomDateTimePicker'
import NameDialog from '../shared/NameDialog'
import { PRIO_CONFIG, katColor, taskInputStyle, taskLabelClass, type Member, type TaskFieldValues } from './todoListModel'
interface TodoTaskFieldsProps {
values: TaskFieldValues
onChange: (patch: Partial<TaskFieldValues>) => void
categories: string[]
members: Member[]
canEdit?: boolean
}
/**
* The fields a task is made of, shared by the detail pane and the new-task
* dialog so both read and behave the same: description, priority as one
* segmented track, the list (a new one is named in a dialog), the due date and
* who does it.
*/
export default function TodoTaskFields({ values, onChange, categories, members, canEdit = true }: TodoTaskFieldsProps) {
const { t } = useTranslation()
const [namingList, setNamingList] = useState(false)
const [listName, setListName] = useState('')
const { desc, priority, category, dueDate, assignedUserId } = values
const confirmList = () => {
const name = listName.trim()
if (!name) return
onChange({ category: name })
setNamingList(false)
setListName('')
}
return (
<>
<div>
<label className={taskLabelClass}>{t('todo.detail.description')}</label>
<textarea value={desc} onChange={e => onChange({ desc: e.target.value })} disabled={!canEdit} rows={4}
placeholder={t('todo.descriptionPlaceholder')}
style={{ ...taskInputStyle, resize: 'vertical', minHeight: 84 }} />
</div>
<div>
<label className={taskLabelClass}>{t('todo.detail.priority')}</label>
<div style={{ display: 'flex', gap: 2, padding: 3, borderRadius: 11, background: 'var(--bg-tertiary)' }}>
{[0, 1, 2, 3].map(p => {
const cfg = PRIO_CONFIG[p]
const isActive = priority === p
return (
<button type="button" key={p} onClick={() => canEdit && onChange({ priority: p })} aria-pressed={isActive}
style={{
flex: 1, height: 28, borderRadius: 8, border: 'none', cursor: canEdit ? 'pointer' : 'default',
fontSize: 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 600, fontFamily: 'inherit',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
background: isActive ? 'var(--bg-card)' : 'transparent',
boxShadow: isActive ? '0 1px 2px rgba(0,0,0,0.1)' : 'none',
color: isActive ? (cfg ? cfg.color : 'var(--text-primary)') : 'var(--text-faint)',
transition: 'all 0.12s',
}}>
{cfg ? <><Flag size={10} />{cfg.label}</> : t('todo.detail.noPriority')}
</button>
)
})}
</div>
</div>
<div>
<label className={taskLabelClass}>{t('todo.detail.category')}</label>
<div style={{ display: 'flex', gap: 6 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<CustomSelect
value={category}
onChange={v => onChange({ category: String(v) })}
options={[
{ value: '', label: t('todo.noCategory') },
...categories.map(c => ({
value: c, label: c,
icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: katColor(c, categories), display: 'inline-block' }} />,
})),
...(category && !categories.includes(category) ? [{
value: category, label: `${category} (${t('todo.newCategoryLabel')})`,
icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: katColor(category, categories), display: 'inline-block' }} />,
}] : []),
]}
placeholder={t('todo.noCategory')}
size="sm"
disabled={!canEdit}
/>
</div>
{canEdit && (
<button type="button" onClick={() => setNamingList(true)} title={t('todo.addCategory')} aria-label={t('todo.addCategory')}
style={{ width: 36, flexShrink: 0, background: 'var(--bg-tertiary)', border: 'none', borderRadius: 10, cursor: 'pointer', color: 'var(--text-muted)', display: 'grid', placeItems: 'center' }}>
<Plus size={14} />
</button>
)}
</div>
</div>
<div>
<label className={taskLabelClass}>{t('todo.detail.dueDate')}</label>
<CustomDatePicker value={dueDate} onChange={v => onChange({ dueDate: v })} />
</div>
<div>
<label className={taskLabelClass}>{t('todo.detail.assignedTo')}</label>
<CustomSelect
value={String(assignedUserId ?? '')}
onChange={v => onChange({ assignedUserId: v ? Number(v) : null })}
options={[
{ value: '', label: t('todo.unassigned'), icon: <User size={14} className="text-content-faint" /> },
...members.map(m => ({
value: String(m.id),
label: m.is_guest ? `${m.username} · ${t('members.guest')}` : m.username,
icon: m.avatar ? (
<img src={avatarSrc(m.avatar)!} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
) : (
<span style={{ width: 18, height: 18, borderRadius: '50%', background: 'var(--bg-tertiary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-muted)', fontWeight: 700 }}>
{m.username.charAt(0).toUpperCase()}
</span>
),
})),
]}
placeholder={t('todo.unassigned')}
size="sm"
disabled={!canEdit}
/>
</div>
<NameDialog
open={namingList}
title={t('todo.addCategory')}
placeholder={t('todo.newCategory')}
confirmLabel={t('common.add')}
value={listName}
onChange={setListName}
onConfirm={confirmList}
onClose={() => { setNamingList(false); setListName('') }}
/>
</>
)
}
@@ -1,3 +1,5 @@
import type { CSSProperties } from 'react'
// Pure constants + helpers + types for the todo list. No React, no side effects.
export const KAT_COLORS = [
@@ -22,3 +24,19 @@ export function katColor(kat: string, allCategories: string[]) {
export type FilterType = 'all' | 'my' | 'overdue' | 'done' | string
export interface Member { id: number; username: string; avatar: string | null; is_guest?: boolean }
/** A task's editable fields as its form holds them: empty strings where the task has nothing. */
export interface TaskFieldValues {
desc: string
priority: number
category: string
dueDate: string
assignedUserId: number | null
}
/** The label and field look of the task form, shared by the detail pane and the new-task dialog. */
export const taskLabelClass = 'mb-1.5 block text-caption font-semibold text-content-muted'
export const taskInputStyle: CSSProperties = {
width: '100%', fontSize: 'calc(13px * var(--fs-scale-body, 1))', padding: '9px 11px', border: '1px solid var(--border-primary)',
borderRadius: 10, background: 'var(--bg-input)', color: 'var(--text-primary)', fontFamily: 'inherit', outline: 'none',
}
@@ -1,21 +1,21 @@
// FE-PACKNAME-001 to FE-PACKNAME-004
// FE-NAMEDLG-001 to FE-NAMEDLG-004
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '../../../tests/helpers/render'
import PackingNameDialog from './PackingNameDialog'
import NameDialog from './NameDialog'
function setup(value: string, open = true) {
const onChange = vi.fn()
const onConfirm = vi.fn()
const onClose = vi.fn()
render(
<PackingNameDialog open={open} title="Add list" placeholder="List name" confirmLabel="Add"
<NameDialog open={open} title="Add list" placeholder="List name" confirmLabel="Add"
value={value} onChange={onChange} onConfirm={onConfirm} onClose={onClose} />,
)
return { onChange, onConfirm, onClose }
}
describe('PackingNameDialog', () => {
it('FE-PACKNAME-001: asks for the name under its title, confirm waiting for one', () => {
describe('NameDialog', () => {
it('FE-NAMEDLG-001: asks for the name under its title, confirm waiting for one', () => {
const { onChange } = setup('')
expect(screen.getByText('Add list')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled()
@@ -24,14 +24,14 @@ describe('PackingNameDialog', () => {
expect(onChange).toHaveBeenCalledWith('Rain kit')
})
it('FE-PACKNAME-002: Enter and the button confirm a typed name', () => {
it('FE-NAMEDLG-002: Enter and the button confirm a typed name', () => {
const { onConfirm } = setup('Rain kit')
fireEvent.keyDown(screen.getByPlaceholderText('List name'), { key: 'Enter' })
fireEvent.click(screen.getByRole('button', { name: 'Add' }))
expect(onConfirm).toHaveBeenCalledTimes(2)
})
it('FE-PACKNAME-003: Enter on a blank name does nothing, Cancel closes', () => {
it('FE-NAMEDLG-003: Enter on a blank name does nothing, Cancel closes', () => {
const { onConfirm, onClose } = setup(' ')
fireEvent.keyDown(screen.getByPlaceholderText('List name'), { key: 'Enter' })
expect(onConfirm).not.toHaveBeenCalled()
@@ -40,7 +40,7 @@ describe('PackingNameDialog', () => {
expect(onClose).toHaveBeenCalledTimes(1)
})
it('FE-PACKNAME-004: stays out of the page while closed', () => {
it('FE-NAMEDLG-004: stays out of the page while closed', () => {
setup('', false)
expect(screen.queryByPlaceholderText('List name')).toBeNull()
})
@@ -1,7 +1,7 @@
import Modal from '../shared/Modal'
import Modal from './Modal'
import { useTranslation } from '../../i18n'
interface PackingNameDialogProps {
interface NameDialogProps {
open: boolean
title: string
placeholder: string
@@ -17,7 +17,7 @@ interface PackingNameDialogProps {
* It takes the name in the middle of the screen instead of pushing a field
* into the page; Enter confirms and Esc (from the modal) cancels.
*/
export default function PackingNameDialog({ open, title, placeholder, confirmLabel, value, onChange, onConfirm, onClose }: PackingNameDialogProps) {
export default function NameDialog({ open, title, placeholder, confirmLabel, value, onChange, onConfirm, onClose }: NameDialogProps) {
const { t } = useTranslation()
const ready = value.trim().length > 0
return (
+6 -3
View File
@@ -135,9 +135,12 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
{ id: 'todo' as const, label: t('todo.subtab.todo'), icon: ListTodo, count: todoItems.length },
]
// The to-do view fills what is left under the bar, so its list and detail pane
// scroll inside the screen and the pane's buttons stay in sight.
const fill = subTab === 'todo'
return (
<div>
<div style={{ padding: '24px 28px 0' }} className="max-md:!px-4 max-md:!pt-4">
<div style={fill ? { display: 'flex', flexDirection: 'column', height: '100%' } : undefined}>
<div style={{ padding: '24px 28px 0', flexShrink: 0 }} className="max-md:!px-4 max-md:!pt-4">
<div className="bg-surface-tertiary" style={{
borderRadius: 18,
padding: '14px 16px 14px 22px',
@@ -237,7 +240,7 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
)}
</div>
</div>
<div style={{ padding: '16px 28px 0' }} className="max-md:!px-4">
<div style={fill ? { padding: '16px 28px 16px', flex: 1, minHeight: 0 } : { padding: '16px 28px 0' }} className="max-md:!px-4">
{subTab === 'packing' && (
<LazyPanel id="packing">
<PackingListPanel tripId={tripId} items={packingItems} openImportSignal={importPackingSignal} addCategorySignal={addCategorySignal} saveTemplateSignal={saveTemplateSignal} inlineHeader={false} view={packingView} onViewChange={setPackingView} />
+6 -5
View File
@@ -2718,7 +2718,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'«تطبيق قالب» و«حفظ كقالب» تملآن قائمة أو تحفظانها دون كتابتها كلها، والأيقونتان بجانبهما تصدّران القائمة كنسخة مطبوعة أو PDF أو ملف، وتستوردان قائمة. والزر الأحمر بجانب شريط التقدّم يذكر كم عنصرًا محددًا ويزيلها.',
'help.ctx.trip-lists.bullet.6':
'لـ«المهام» شريط جانبي خاص بها: بطاقة التقدّم، والفلاتر «الكل» و«مهامي» و«متأخرة» و«منجز»، و«ترتيب حسب» «الأولوية» أو «تاريخ الاستحقاق»، وصف واحد لكل قائمة. والنقر على مهمة يفتحها في اللوحة على اليمين، و«إضافة مهمة جديدة» تفتح نموذج «مهمة جديدة» فوق وسط الشاشة.',
'لـ«المهام» شريط جانبي خاص بها: بطاقة التقدّم، والفلاتر «الكل» و«مهامي» و«متأخرة» و«منجز»، وصف واحد لكل قائمة وتحتها «إضافة قائمة». وتقع المهام في بطاقة يحمل رأسها اسم الفلتر ويضم الترتيب، «الأولوية» أو «تاريخ الاستحقاق». والنقر على مهمة يفتحها في اللوحة على اليمين، و«إضافة مهمة جديدة» تفتح نموذج «مهمة جديدة» فوق وسط الشاشة.',
// packing-categories
'help.guide.packing-categories.title': 'بناء قائمة التجهيز',
'help.guide.packing-categories.goal': 'اجمع ما تأخذه في قوائم، واملأها بالعناصر، وحدّد من يتولى كل قائمة.',
@@ -2845,7 +2845,8 @@ const help: TranslationStrings = {
'help.guide.create-todo.goal': 'دوّن شيئًا يجب أن يحدث، مع قائمة وأولوية وتاريخ واسم مقابله.',
'help.guide.create-todo.step.1': 'انقر «إضافة مهمة جديدة» في أعلى اليمين.',
'help.guide.create-todo.step.2': 'سمّها في «اسم المهمة»، وضع كل ما يستحق التذكّر تحت «وصف».',
'help.guide.create-todo.step.3': '«قائمة» تجمع المهمة. اختر واحدة، أو استخدم علامة الزائد بجانبها لكتابة اسم جديد.',
'help.guide.create-todo.step.3':
'«قائمة» تجمع المهمة. اختر واحدة، أو استخدم علامة الزائد بجانبها لتسمية قائمة جديدة في نافذة حوار صغيرة.',
'help.guide.create-todo.step.4': '«الأولوية» أربعة أزرار: «لا شيء» وP1 وP2 وP3، من الأحمر نزولًا إلى الأزرق.',
'help.guide.create-todo.step.5': '«تاريخ الاستحقاق» يفتح تقويمًا، و«مسند إلى» يضع اسمًا على المهمة.',
'help.guide.create-todo.step.6': 'انقر «إنشاء مهمة».',
@@ -2862,15 +2863,15 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'تحت «القوائم» صف واحد لكل قائمة. واختيار واحدة يعرض تلك القائمة، بما فيها المهام المنتهية.',
'help.guide.todo-filters.step.3':
'«ترتيب حسب» يعيد ترتيب ما على الشاشة: «الأولوية» تضع P1 أولًا، و«تاريخ الاستحقاق» يضع أقرب موعد أولًا. واحد منهما فقط في كل مرة.',
'الترتيب في رأس القائمة يعيد ترتيب ما على الشاشة: «الأولوية» تضع P1 أولًا، و«تاريخ الاستحقاق» يضع أقرب موعد أولًا. واحد منهما فقط في كل مرة، ونقرة ثانية تعيدك إلى ترتيبك الخاص.',
'help.guide.todo-filters.step.4': 'انقر مهمة لتفتحها في اللوحة على اليمين.',
'help.guide.todo-filters.step.5':
'غيّر ما تحتاجه، «وصف» أو «الأولوية» أو «قائمة» أو «تاريخ الاستحقاق» أو «مسند إلى»، ثم «حفظ التغييرات». و«حذف» يأخذ المهمة فورًا.',
'غيّر ما تحتاجه، «وصف» أو «الأولوية» أو «قائمة» أو «تاريخ الاستحقاق» أو «مسند إلى»، ثم «حفظ التغييرات». والمربع في رأس اللوحة يعلّم المهمة كمنجزة، و«حذف» يأخذها فورًا.',
'help.guide.todo-filters.result': 'القائمة تعرض المهام التي طلبتها فقط، واللوحة على اليمين تعدّل التي اخترتها.',
'help.guide.todo-filters.tip.1':
'صف القائمة لا يعدّ إلا ما ما زال مفتوحًا، لكن اختياره يعرض المهام المنتهية أيضًا. و«الكل» و«مهامي» و«متأخرة» تخفي ما أُنجز؛ و«منجز» لا يعرض غيره.',
'help.guide.todo-filters.tip.2':
'«ترتيب حسب» «الأولوية» و«ترتيب حسب» «تاريخ الاستحقاق» يستبعد كل منهما الآخر، وما دام أحدهما مفعّلًا لا يمكن سحب الصفوف إلى ترتيب خاص بك.',
'«الأولوية» و«تاريخ الاستحقاق» في الترتيب يستبعد كل منهما الآخر، وما دام أحدهما مفعّلًا لا يمكن سحب الصفوف إلى ترتيب خاص بك.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'الحجوزات',
+5 -5
View File
@@ -3131,7 +3131,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Şablonu tətbiq et və Şablon kimi yadda saxla siyahını əl ilə yazmadan doldurur və ya saxlayır, yanlarındakı iki ikon isə siyahını çap, PDF və ya fayl kimi ixrac edir və bir siyahı idxal edir. İrəliləyiş zolağının yanındakı qırmızı düymə neçə elementin işarələndiyini göstərir və onları silir.',
'help.ctx.trip-lists.bullet.6':
'Tapşırıqların öz yan paneli var: irəliləyiş kartı, Hamısı, Mənim tapşırıqlarım, Vaxtı keçmiş və Hazır filtrləri, Prioritet və ya Son tarix üzrə Sırala və hər siyahı üçün bir sətir. Tapşırığa klikləmək onu sağdakı paneldə açır, Yeni tapşırıq əlavə et isə ekranın ortasında Yeni tapşırıq formasını açır.',
'Tapşırıqların öz yan paneli var: irəliləyiş kartı, Hamısı, Mənim tapşırıqlarım, Vaxtı keçmiş və Tamamlanıb filtrləri, hər siyahı üçün bir sətir və onların altında Siyahı əlavə et. Tapşırıqlar bir kartın içindədir; kartın başlığında filtrin adı və sıralama, yəni Prioritet və ya Son tarix, yer alır. Tapşırığa klikləmək onu sağdakı paneldə açır, Yeni tapşırıq əlavə et isə ekranın ortasında Yeni tapşırıq formasını açır.',
// packing-categories
'help.guide.packing-categories.title': 'Baqaj siyahısını qurun',
'help.guide.packing-categories.goal':
@@ -3275,7 +3275,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Tapşırığın adı sahəsində ona ad verin, yadda saxlanmağa dəyər hər şeyi isə Təsvir sahəsinə yazın.',
'help.guide.create-todo.step.3':
'Siyahı tapşırığı qruplaşdırır. Birini seçin və ya yeni ad yazmaq üçün yanındakı artı işarəsindən istifadə edin.',
'Siyahı tapşırığı qruplaşdırır. Birini seçin və ya yanındakı artı işarəsi ilə kiçik bir dialoqda yenisinə ad verin.',
'help.guide.create-todo.step.4':
'Prioritet dörd düymədən ibarətdir: Heç biri, P1, P2 və P3, qırmızıdan maviyə doğru.',
'help.guide.create-todo.step.5': 'Son tarix təqvim açır, Təyin edilən şəxslər isə tapşırığa şəxs təyin edir.',
@@ -3295,16 +3295,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Siyahılar altında hər siyahı üçün bir sətir var. Birini seçdikdə həmin siyahı tamamlanmış tapşırıqlarla birlikdə göstərilir.',
'help.guide.todo-filters.step.3':
'Sırala ekrandakıların sırasını dəyişir: Prioritet P1-i birinci qoyur, Son tarix ən yaxın son tarixi birinci qoyur. Eyni anda bu ikisindən yalnız biri aktiv ola bilər.',
'Siyahının başlığındakı sıralama ekrandakıların sırasını dəyişir: Prioritet P1-i birinci qoyur, Son tarix ən yaxın son tarixi birinci qoyur. Eyni anda bu ikisindən yalnız biri aktiv ola bilər, ikinci klik isə sizin öz sıranıza qaytarır.',
'help.guide.todo-filters.step.4': 'Tapşırığı sağdakı paneldə açmaq üçün ona klikləyin.',
'help.guide.todo-filters.step.5':
'Lazım olanı dəyişin: Təsvir, Prioritet, Siyahı, Son tarix və ya Təyin edilən şəxslər, sonra Dəyişiklikləri yadda saxla. Sil tapşırığı dərhal silir.',
'Lazım olanı dəyişin: Təsvir, Prioritet, Siyahı, Son tarix və ya Təyin edilən şəxslər, sonra Dəyişiklikləri yadda saxla. Panelin başlığındakı qutu tapşırığı tamamlanmış kimi işarələyir, Sil isə onu dərhal silir.',
'help.guide.todo-filters.result':
'Siyahı yalnız istədiyiniz tapşırıqları göstərir, sağdakı panel isə seçdiyiniz tapşırığı redaktə edir.',
'help.guide.todo-filters.tip.1':
'Siyahı sətri yalnız hələ açıq olanları sayır, lakin onu seçdikdə tamamlanmış tapşırıqlar da göstərilir. Hamısı, Mənim tapşırıqlarım və Vaxtı keçmiş tamamlananları gizlədir; Hazır isə yalnız onları göstərir.',
'help.guide.todo-filters.tip.2':
'Prioritetə görə və Son tarixə görə sıralama bir-birini istisna edir; onlardan biri aktiv olduqda sətirləri sürükləyərək öz sıranızı qurmaq mümkün olmur.',
'Sıralamadakı Prioritet və Son tarix bir-birini istisna edir; onlardan biri aktiv olduqda sətirləri sürükləyərək öz sıranızı qurmaq mümkün olmur.',
// ── Screen: trip-costs ────────────────────────────────────────────────────────────────
'help.ctx.trip-costs.title': 'Xərclər',
+5 -5
View File
@@ -2969,7 +2969,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Aplicar modelo e Salvar como modelo preenchem ou guardam uma lista sem digitá-la, e os dois ícones ao lado exportam a lista, como impressão, PDF ou arquivo, e importam uma. O botão vermelho ao lado da barra de progresso diz quantos itens estão marcados e os retira.',
'help.ctx.trip-lists.bullet.6':
'A fazer tem uma barra lateral própria: o cartão de progresso, os filtros Todos, Minhas tarefas, Atrasada e Concluído, Ordenar por Prioridade ou Data de vencimento, e uma linha por lista. Um clique numa tarefa a abre no painel da direita, e Nova tarefa abre o formulário Nova tarefa sobre o meio da tela.',
'A fazer tem uma barra lateral própria: o cartão de progresso, os filtros Todos, Minhas tarefas, Atrasada e Concluído, uma linha por lista e Adicionar lista embaixo delas. As tarefas ficam num cartão cujo cabeçalho nomeia o filtro e traz a ordenação, Prioridade ou Data de vencimento. Um clique numa tarefa a abre no painel da direita, e Nova tarefa abre o formulário Nova tarefa sobre o meio da tela.',
// packing-categories
'help.guide.packing-categories.title': 'Montar a lista de mala',
'help.guide.packing-categories.goal':
@@ -3112,7 +3112,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Dê um nome a ela em Nome da tarefa, e ponha em Descrição tudo o que valer a pena lembrar.',
'help.guide.create-todo.step.3':
'Lista agrupa a tarefa. Escolha uma, ou use o sinal de mais ao lado para digitar um nome novo.',
'Lista agrupa a tarefa. Escolha uma, ou use o sinal de mais ao lado para dar nome a uma nova numa pequena caixa de diálogo.',
'help.guide.create-todo.step.4': 'Prioridade são quatro botões: Nenhuma, P1, P2 e P3, do vermelho ao azul.',
'help.guide.create-todo.step.5': 'Data de vencimento abre um calendário, e Atribuído a põe um nome na tarefa.',
'help.guide.create-todo.step.6': 'Clique em Criar tarefa.',
@@ -3131,16 +3131,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Sob Listas fica uma linha por lista. Escolher uma mostra aquela lista, tarefas concluídas inclusive.',
'help.guide.todo-filters.step.3':
'Ordenar por reordena o que está na tela: Prioridade põe P1 primeiro, Data de vencimento põe o prazo mais próximo primeiro. Só um dos dois por vez.',
'A ordenação no cabeçalho da lista reordena o que está na tela: Prioridade põe P1 primeiro, Data de vencimento põe o prazo mais próximo primeiro. Só um dos dois por vez, e um segundo clique volta para a sua própria ordem.',
'help.guide.todo-filters.step.4': 'Clique numa tarefa para abri-la no painel da direita.',
'help.guide.todo-filters.step.5':
'Mude o que precisar, Descrição, Prioridade, Lista, Data de vencimento ou Atribuído a, depois Salvar alterações. Excluir leva a tarefa embora na hora.',
'Mude o que precisar, Descrição, Prioridade, Lista, Data de vencimento ou Atribuído a, depois Salvar alterações. A caixa no cabeçalho do painel marca a tarefa como feita, e Excluir a leva embora na hora.',
'help.guide.todo-filters.result':
'A lista mostra só as tarefas que você pediu, e o painel da direita edita a que você escolheu.',
'help.guide.todo-filters.tip.1':
'Uma linha de lista conta só o que continua em aberto, mas selecioná-la mostra também as tarefas concluídas. Todos, Minhas tarefas e Atrasada escondem o que está pronto; Concluído não mostra mais nada.',
'help.guide.todo-filters.tip.2':
'Ordenar por Prioridade e Ordenar por Data de vencimento se excluem, e enquanto um dos dois estiver ligado as linhas não podem mais ser arrastadas para uma ordem sua.',
'Prioridade e Data de vencimento na ordenação se excluem, e enquanto um dos dois estiver ligado as linhas não podem mais ser arrastadas para uma ordem sua.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Reservas',
+5 -5
View File
@@ -3021,7 +3021,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Aplica una plantilla i Desa com a plantilla omplen o conserven una llista sense teclejar-la, i les dues icones del costat exporten la llista, com a impressió, PDF o fitxer, i n’importen una. El botó vermell al costat de la barra de progrés diu quants articles estan marcats i els retira.',
'help.ctx.trip-lists.bullet.6':
'Pendents té una barra lateral pròpia: la targeta de progrés, els filtres Tot, Les meves tasques, Vençuda i Fet, Ordena per Prioritat o Data límit, i una fila per categoria. Un clic en una tasca l’obre al panell de la dreta, i Tasca nova obre el formulari Tasca nova sobre el mig de la pantalla.',
'Pendents té una barra lateral pròpia: la targeta de progrés, els filtres Tot, Les meves tasques, Vençuda i Fet, una fila per categoria i, a sota, Afegeix una categoria. Les tasques són en una targeta amb una capçalera que anomena el filtre i porta l’ordenació, Prioritat o Data límit. Un clic en una tasca l’obre al panell de la dreta, i Tasca nova obre el formulari Tasca nova sobre el mig de la pantalla.',
// packing-categories
'help.guide.packing-categories.title': 'Construir la llista d’equipatge',
'help.guide.packing-categories.goal':
@@ -3168,7 +3168,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Posa-li nom a Nom de la tasca, i escriu sota Descripció tot el que valgui la pena recordar.',
'help.guide.create-todo.step.3':
'Categoria agrupa la tasca. Tria’n una, o fes servir el signe més del costat per escriure un nom nou.',
'Categoria agrupa la tasca. Tria’n una, o fes servir el signe més del costat per posar nom a una de nova en un petit diàleg.',
'help.guide.create-todo.step.4': 'Prioritat són quatre botons: Cap, P1, P2 i P3, del vermell al blau.',
'help.guide.create-todo.step.5': 'Data límit obre un calendari, i Assignada a posa un nom a la tasca.',
'help.guide.create-todo.step.6': 'Fes clic a Crea una tasca.',
@@ -3188,16 +3188,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Sota Categories hi ha una fila per categoria. Triar-ne una mostra aquella categoria, tasques acabades incloses.',
'help.guide.todo-filters.step.3':
'Ordena per reordena el que hi ha a la pantalla: Prioritat posa P1 primer, Data límit posa el termini més proper primer. Només un dels dos alhora.',
'L’ordenació a la capçalera de la llista reordena el que hi ha a la pantalla: Prioritat posa P1 primer, Data límit posa el termini més proper primer. Només un dels dos alhora, i un segon clic torna al teu propi ordre.',
'help.guide.todo-filters.step.4': 'Fes clic en una tasca per obrir-la al panell de la dreta.',
'help.guide.todo-filters.step.5':
'Canvia el que et calgui, Descripció, Prioritat, Categoria, Data límit o Assignada a, i després Desa els canvis. Elimina s’emporta la tasca a l’instant.',
'Canvia el que et calgui, Descripció, Prioritat, Categoria, Data límit o Assignada a, i després Desa els canvis. La casella de la capçalera del panell marca la tasca com a feta, i Elimina se l’emporta a l’instant.',
'help.guide.todo-filters.result':
'La llista mostra només les tasques que has demanat, i el panell de la dreta edita la que has triat.',
'help.guide.todo-filters.tip.1':
'Una fila de categoria compta només el que encara és obert, però seleccionar-la mostra també les tasques acabades. Tot, Les meves tasques i Vençuda amaguen el que està fet; Fet no mostra res més.',
'help.guide.todo-filters.tip.2':
'Ordena per Prioritat i Ordena per Data límit s’exclouen, i mentre un dels dos està actiu les files ja no es poden arrossegar a un ordre propi.',
'Prioritat i Data límit a l’ordenació s’exclouen, i mentre un dels dos està actiu les files ja no es poden arrossegar a un ordre propi.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Reserves',
+5 -5
View File
@@ -2899,7 +2899,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Použít šablonu a Uložit jako šablonu naplní nebo zachovají seznam bez psaní a dvě ikony vedle nich seznam exportují, jako výtisk, PDF nebo soubor, nebo nějaký importují. Červené tlačítko u pruhu postupu říká, kolik položek je zaškrtnutých, a odklidí je.',
'help.ctx.trip-lists.bullet.6':
'Úkoly mají vlastní postranní panel: kartu postupu, filtry Vše, Moje úkoly, Po termínu a Hotové, Řadit podle Priorita nebo Termín splnění a jeden řádek na seznam. Kliknutí na úkol ho otevře v panelu vpravo a Přidat nový úkol otevře uprostřed obrazovky formulář Nový úkol.',
'Úkoly mají vlastní postranní panel: kartu postupu, filtry Vše, Moje úkoly, Po termínu a Hotové, jeden řádek na seznam a pod nimi Přidat seznam. Úkoly leží v kartě, jejíž záhlaví jmenuje filtr a nese řazení, Priorita nebo Termín splnění. Kliknutí na úkol ho otevře v panelu vpravo a Přidat nový úkol otevře uprostřed obrazovky formulář Nový úkol.',
// packing-categories
'help.guide.packing-categories.title': 'Sestavit seznam věcí',
'help.guide.packing-categories.goal':
@@ -3035,7 +3035,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.1': 'Klikněte vpravo nahoře na Přidat nový úkol.',
'help.guide.create-todo.step.2': 'Pojmenujte ho v Název úkolu a dejte všechno, co stojí za zapamatování, pod Popis.',
'help.guide.create-todo.step.3':
'Seznam úkol zařazuje. Vyberte jeden, nebo použijte plus vedle něj a napište nový název.',
'Seznam úkol zařazuje. Vyberte jeden, nebo použijte plus vedle něj a v malém dialogu pojmenujte nový.',
'help.guide.create-todo.step.4': 'Priorita jsou čtyři tlačítka: Žádná, P1, P2 a P3, od červené po modrou.',
'help.guide.create-todo.step.5': 'Termín splnění otevře kalendář a Přiřazeno dá na úkol jméno.',
'help.guide.create-todo.step.6': 'Klikněte na Vytvořit úkol.',
@@ -3053,16 +3053,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Pod Seznamy sedí jeden řádek na seznam. Když jeden vyberete, ukáže se ten seznam, včetně dokončených úkolů.',
'help.guide.todo-filters.step.3':
'Řadit podle mění pořadí toho, co je na obrazovce: Priorita dává dopředu P1, Termín splnění dává dopředu nejbližší termín. Vždy jen jedno z těch dvou.',
'Řazení v záhlaví seznamu mění pořadí toho, co je na obrazovce: Priorita dává dopředu P1, Termín splnění dává dopředu nejbližší termín. Vždy jen jedno z těch dvou a druhé kliknutí vrátí vaše vlastní pořadí.',
'help.guide.todo-filters.step.4': 'Klikněte na úkol, abyste ho otevřeli v panelu vpravo.',
'help.guide.todo-filters.step.5':
'Změňte, co potřebujete, Popis, Priorita, Seznam, Termín splnění nebo Přiřazeno, a pak Uložit změny. Smazat úkol odstraní hned.',
'Změňte, co potřebujete, Popis, Priorita, Seznam, Termín splnění nebo Přiřazeno, a pak Uložit změny. Zaškrtávací políčko v záhlaví panelu označí úkol jako hotový a Smazat ho odstraní hned.',
'help.guide.todo-filters.result':
'Seznam ukazuje jen úkoly, na které jste se ptali, a panel vpravo upravuje ten, který jste vybrali.',
'help.guide.todo-filters.tip.1':
'Řádek seznamu počítá jen to, co je ještě otevřené, ale když ho vyberete, ukáže i dokončené úkoly. Vše, Moje úkoly a Po termínu skrývají to, co je hotové; Hotové neukazují nic jiného.',
'help.guide.todo-filters.tip.2':
'Řadit podle Priorita a Řadit podle Termín splnění se navzájem vylučují a dokud je jedno z nich zapnuté, řádky už nejde přetáhnout do vlastního pořadí.',
'Priorita a Termín splnění v řazení se navzájem vylučují a dokud je jedno z nich zapnuté, řádky už nejde přetáhnout do vlastního pořadí.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Rezervace',
+5 -5
View File
@@ -3051,7 +3051,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Vorlage anwenden und Als Vorlage speichern füllen oder bewahren eine Liste, ohne sie abzutippen, und die zwei Symbole daneben exportieren die Liste, als Ausdruck, als PDF oder als Datei, und importieren eine. Der rote Knopf neben dem Fortschrittsbalken nennt, wie viele Einträge abgehakt sind, und räumt sie weg.',
'help.ctx.trip-lists.bullet.6':
'Aufgaben hat eine eigene Seitenleiste: die Fortschrittskarte, die Filter Alle, Meine Aufgaben, Überfällig und Erledigt, Sortieren nach Priorität oder Fällig am, und eine Zeile pro Liste. Ein Klick auf eine Aufgabe öffnet sie im Panel rechts, und Neue Aufgabe hinzufügen öffnet das Formular Neue Aufgabe über der Mitte des Bildschirms.',
'Aufgaben hat eine eigene Seitenleiste: die Fortschrittskarte, die Filter Alle, Meine Aufgaben, Überfällig und Erledigt, eine Zeile pro Liste und darunter Liste hinzufügen. Die Aufgaben sitzen in einer Karte, deren Kopf den Filter nennt und die Sortierung trägt, Priorität oder Fällig am. Ein Klick auf eine Aufgabe öffnet sie im Panel rechts, und Neue Aufgabe hinzufügen öffnet das Formular Neue Aufgabe über der Mitte des Bildschirms.',
// packing-categories
'help.guide.packing-categories.title': 'Die Packliste aufbauen',
'help.guide.packing-categories.goal':
@@ -3201,7 +3201,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Benenne sie in Aufgabenname, und schreib alles, was du dir merken willst, unter Beschreibung.',
'help.guide.create-todo.step.3':
'Liste gruppiert die Aufgabe. Wähl eine, oder tipp mit dem Plus daneben einen neuen Namen.',
'Liste gruppiert die Aufgabe. Wähl eine, oder benenn über das Plus daneben in einem kleinen Dialog eine neue.',
'help.guide.create-todo.step.4': 'Priorität sind vier Knöpfe: Keine, P1, P2 und P3, von Rot bis Blau.',
'help.guide.create-todo.step.5': 'Fällig am öffnet einen Kalender, und Zuständig setzt einen Namen auf die Aufgabe.',
'help.guide.create-todo.step.6': 'Klick auf Aufgabe erstellen.',
@@ -3221,16 +3221,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Unter Listen sitzt eine Zeile pro Liste. Eine davon zu wählen zeigt diese Liste, erledigte Aufgaben eingeschlossen.',
'help.guide.todo-filters.step.3':
'Sortieren nach ordnet um, was auf dem Schirm ist: Priorität stellt P1 nach vorn, Fällig am die nächste Frist. Immer nur eins von beiden.',
'Die Sortierung im Kopf der Liste ordnet um, was auf dem Schirm ist: Priorität stellt P1 nach vorn, Fällig am die nächste Frist. Immer nur eins von beiden, und ein zweiter Klick führt zurück zu deiner eigenen Reihenfolge.',
'help.guide.todo-filters.step.4': 'Klick auf eine Aufgabe, um sie im Panel rechts zu öffnen.',
'help.guide.todo-filters.step.5':
'Änder, was du brauchst, Beschreibung, Priorität, Liste, Fällig am oder Zuständig, dann Speichern. Löschen nimmt die Aufgabe sofort weg.',
'Änder, was du brauchst, Beschreibung, Priorität, Liste, Fällig am oder Zuständig, dann Speichern. Das Kästchen im Kopf des Panels hakt die Aufgabe ab, und Löschen nimmt sie sofort weg.',
'help.guide.todo-filters.result':
'Die Liste zeigt nur die Aufgaben, nach denen du gefragt hast, und das Panel rechts bearbeitet die, die du gewählt hast.',
'help.guide.todo-filters.tip.1':
'Eine Listenzeile zählt nur, was noch offen ist, aber sie auszuwählen zeigt auch die erledigten Aufgaben. Alle, Meine Aufgaben und Überfällig verbergen, was erledigt ist; Erledigt zeigt nichts anderes.',
'help.guide.todo-filters.tip.2':
'Sortieren nach Priorität und Sortieren nach Fällig am schließen sich aus, und solange eins von beiden an ist, lassen sich die Zeilen nicht mehr in eine eigene Reihenfolge ziehen.',
'Priorität und Fällig am in der Sortierung schließen sich aus, und solange eins von beiden an ist, lassen sich die Zeilen nicht mehr in eine eigene Reihenfolge ziehen.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Buchungen',
+6 -5
View File
@@ -2987,7 +2987,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Apply template and Save as template fill or keep a list without typing it out, and the two icons beside them export the list, as a printout, a PDF or a file, and import one. The red button beside the progress bar names how many items are ticked and clears them away.',
'help.ctx.trip-lists.bullet.6':
'To-Do has a sidebar of its own: the progress card, the filters All, My Tasks, Overdue and Done, Sort by Priority or Due date, and one row per list. A click on a task opens it in the pane on the right, and Add new task opens the New task form over the middle of the screen.',
'To-Do has a sidebar of its own: the progress card, the filters All, My Tasks, Overdue and Done, one row per list and Add list under them. The tasks sit in a card whose head names the filter and holds the sort, Priority or Due date. A click on a task opens it in the pane on the right, and Add new task opens the New task form over the middle of the screen.',
// packing-categories
'help.guide.packing-categories.title': 'Build the packing list',
'help.guide.packing-categories.goal':
@@ -3125,7 +3125,8 @@ const help: TranslationStrings = {
'Write down something that has to happen, with a list, a priority, a date and a name against it.',
'help.guide.create-todo.step.1': 'Click Add new task at the top right.',
'help.guide.create-todo.step.2': 'Name it in Task name, and put anything worth remembering under Description.',
'help.guide.create-todo.step.3': 'List groups the task. Pick one, or use the plus next to it to type a new name.',
'help.guide.create-todo.step.3':
'List groups the task. Pick one, or use the plus next to it to name a new one in a small dialog.',
'help.guide.create-todo.step.4': 'Priority is four buttons: None, P1, P2 and P3, red down to blue.',
'help.guide.create-todo.step.5': 'Due date opens a calendar, and Assigned to puts a name on the task.',
'help.guide.create-todo.step.6': 'Click Create task.',
@@ -3144,16 +3145,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Under Lists sits one row per list. Picking one shows that list, finished tasks included.',
'help.guide.todo-filters.step.3':
'Sort by reorders what is on screen: Priority puts P1 first, Due date puts the nearest deadline first. Only one of the two at a time.',
'The sort in the head of the list reorders what is on screen: Priority puts P1 first, Due date puts the nearest deadline first. Only one of the two at a time, and a second click goes back to your own order.',
'help.guide.todo-filters.step.4': 'Click a task to open it in the pane on the right.',
'help.guide.todo-filters.step.5':
'Change what you need, Description, Priority, List, Due date or Assigned to, then Save changes. Delete takes the task away at once.',
'Change what you need, Description, Priority, List, Due date or Assigned to, then Save changes. The box in the pane’s head ticks the task off, and Delete takes it away at once.',
'help.guide.todo-filters.result':
'The list shows only the tasks you asked for, and the pane on the right edits the one you picked.',
'help.guide.todo-filters.tip.1':
'A list row counts only what is still open, but selecting it shows the finished tasks too. All, My Tasks and Overdue hide what is done; Done shows nothing else.',
'help.guide.todo-filters.tip.2':
'Sort by Priority and Sort by Due date exclude each other, and while either is on the rows can no longer be dragged into an order of your own.',
'Priority and Due date in the sort exclude each other, and while either is on the rows can no longer be dragged into an order of your own.',
// ── Screen: trip-costs ────────────────────────────────────────────────────────────────
'help.ctx.trip-costs.title': 'Costs',
+5 -5
View File
@@ -3033,7 +3033,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Aplicar plantilla y Guardar como plantilla llenan o conservan una lista sin teclearla, y los dos iconos de al lado exportan la lista, como impresión, PDF o archivo, e importan una. El botón rojo junto a la barra de progreso dice cuántos artículos están marcados y los quita.',
'help.ctx.trip-lists.bullet.6':
'Por hacer tiene su propia barra lateral: la tarjeta de progreso, los filtros Todo, Mis tareas, Vencida y Hecho, Ordenar por Prioridad o Fecha límite, y una fila por lista. Un clic en una tarea la abre en el panel de la derecha, y Nueva tarea abre el formulario Nueva tarea sobre el centro de la pantalla.',
'Por hacer tiene su propia barra lateral: la tarjeta de progreso, los filtros Todo, Mis tareas, Vencida y Hecho, una fila por lista y Añadir lista debajo. Las tareas están en una tarjeta cuya cabecera nombra el filtro y contiene el orden, Prioridad o Fecha límite. Un clic en una tarea la abre en el panel de la derecha, y Nueva tarea abre el formulario Nueva tarea sobre el centro de la pantalla.',
// packing-categories
'help.guide.packing-categories.title': 'Construir la lista de equipaje',
'help.guide.packing-categories.goal':
@@ -3182,7 +3182,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Ponle nombre en Nombre de la tarea, y escribe lo que valga la pena recordar en Descripción.',
'help.guide.create-todo.step.3':
'Lista agrupa la tarea. Elige una, o usa el signo más de al lado para escribir un nombre nuevo.',
'Lista agrupa la tarea. Elige una, o usa el signo más de al lado para darle nombre a una nueva en un pequeño diálogo.',
'help.guide.create-todo.step.4': 'Prioridad son cuatro botones: Ninguna, P1, P2 y P3, del rojo al azul.',
'help.guide.create-todo.step.5': 'Fecha límite abre un calendario, y Asignado a pone un nombre en la tarea.',
'help.guide.create-todo.step.6': 'Haz clic en Crear tarea.',
@@ -3202,16 +3202,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Bajo Listas hay una fila por lista. Elegir una muestra esa lista, tareas terminadas incluidas.',
'help.guide.todo-filters.step.3':
'Ordenar por reordena lo que hay en pantalla: Prioridad pone P1 primero, Fecha límite pone el plazo más cercano primero. Solo uno de los dos a la vez.',
'El orden en la cabecera de la lista reordena lo que hay en pantalla: Prioridad pone P1 primero, Fecha límite pone el plazo más cercano primero. Solo uno de los dos a la vez, y un segundo clic vuelve a tu propio orden.',
'help.guide.todo-filters.step.4': 'Haz clic en una tarea para abrirla en el panel de la derecha.',
'help.guide.todo-filters.step.5':
'Cambia lo que necesites, Descripción, Prioridad, Lista, Fecha límite o Asignado a, y luego Guardar cambios. Eliminar se lleva la tarea al instante.',
'Cambia lo que necesites, Descripción, Prioridad, Lista, Fecha límite o Asignado a, y luego Guardar cambios. La casilla en la cabecera del panel marca la tarea como hecha, y Eliminar se la lleva al instante.',
'help.guide.todo-filters.result':
'La lista muestra solo las tareas que has pedido, y el panel de la derecha edita la que elegiste.',
'help.guide.todo-filters.tip.1':
'Una fila de lista cuenta solo lo que sigue abierto, pero seleccionarla muestra también las tareas terminadas. Todo, Mis tareas y Vencida ocultan lo hecho; Hecho no muestra nada más.',
'help.guide.todo-filters.tip.2':
'Ordenar por Prioridad y Ordenar por Fecha límite se excluyen, y mientras uno de los dos esté activo las filas ya no se pueden arrastrar a un orden propio.',
'Prioridad y Fecha límite en el orden se excluyen, y mientras uno de los dos esté activo las filas ya no se pueden arrastrar a un orden propio.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Reservas',
+5 -5
View File
@@ -3045,7 +3045,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Rakenda mall ja Salvesta mallina täidavad või salvestavad nimekirja ilma seda käsitsi sisestamata ning nende kõrval olevad kaks ikooni ekspordivad nimekirja väljatrükina, PDF-ina või failina ja impordivad nimekirja. Edenemisriba kõrval olev punane nupp näitab, mitu eset on märgitud, ja eemaldab need.',
'help.ctx.trip-lists.bullet.6':
'Ülesannetel on oma külgriba: edenemise kaart, filtrid Kõik, Minu ülesanded, Üle tähtaja ja Valmis, Sordi valikutega Tähtsus või Tähtaeg ning üks rida iga nimekirja kohta. Klõps ülesandel avab selle paremal paanil ja Lisa uus ülesanne avab ekraani keskel vormi Uus ülesanne.',
'Ülesannetel on oma külgriba: edenemise kaart, filtrid Kõik, Minu ülesanded, Üle tähtaja ja Tehtud, üks rida iga nimekirja kohta ning nende all Lisa nimekiri. Ülesanded on kaardil, mille päis nimetab filtri ja hoiab sortimist, Tähtsus või Tähtaeg. Klõps ülesandel avab selle paremal paanil ja Lisa uus ülesanne avab ekraani keskel vormi Uus ülesanne.',
// packing-categories
'help.guide.packing-categories.title': 'Koosta pakkimisnimekiri',
'help.guide.packing-categories.goal':
@@ -3185,7 +3185,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Anna sellele nimi väljal Ülesande nimi ja kirjuta kõik meelespidamist väärt väljale Kirjeldus.',
'help.guide.create-todo.step.3':
'Loend rühmitab ülesande. Vali üks või kasuta selle kõrval olevat plussi, et sisestada uus nimi.',
'Nimekiri rühmitab ülesande. Vali üks või kasuta selle kõrval olevat plussi, et väikeses dialoogis uuele nimi anda.',
'help.guide.create-todo.step.4': 'Tähtsus koosneb neljast nupust: Puudub, P1, P2 ja P3, punasest siniseni.',
'help.guide.create-todo.step.5': 'Tähtaeg avab kalendri ja Vastutaja määrab ülesandele inimese.',
'help.guide.create-todo.step.6': 'Klõpsa nuppu Loo ülesanne.',
@@ -3202,16 +3202,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Jaotises Nimekirjad on iga loendi kohta üks rida. Ühe valimine näitab seda loendit koos lõpetatud ülesannetega.',
'help.guide.todo-filters.step.3':
'Sordi muudab ekraanil oleva järjekorda: Tähtsus paneb ette P1, Tähtaeg paneb ette lähima tähtaja. Korraga saab kasutada ainult üht neist.',
'Sortimine nimekirja päises muudab ekraanil oleva järjekorda: Tähtsus paneb ette P1, Tähtaeg paneb ette lähima tähtaja. Korraga saab kasutada ainult üht neist ja teine klõps viib tagasi sinu enda järjekorra juurde.',
'help.guide.todo-filters.step.4': 'Klõpsa ülesandel, et see paremal paanil avada.',
'help.guide.todo-filters.step.5':
'Muuda vajalikku, Kirjeldus, Tähtsus, Loend, Tähtaeg või Vastutaja, ja klõpsa Salvesta muudatused. Kustuta eemaldab ülesande kohe.',
'Muuda vajalikku, Kirjeldus, Tähtsus, Nimekiri, Tähtaeg või Vastutaja, ja klõpsa Salvesta muudatused. Paani päises olev märkeruut märgib ülesande tehtuks ja Kustuta eemaldab selle kohe.',
'help.guide.todo-filters.result':
'Loendis on ainult soovitud ülesanded ja paremal paanil saad valitud ülesannet muuta.',
'help.guide.todo-filters.tip.1':
'Loendi rida loeb ainult veel avatud ülesandeid, kuid selle valimisel on näha ka lõpetatud ülesanded. Kõik, Minu ülesanded ja Üle tähtaja peidavad lõpetatud ülesanded; Valmis näitab ainult neid.',
'help.guide.todo-filters.tip.2':
'Sortimine tähtsuse ja tähtaja järgi välistavad teineteist ning kui üks neist on sees, ei saa ridu enam lohistades oma järjekorda seada.',
'Tähtsus ja Tähtaeg sortimises välistavad teineteist ning kui üks neist on sees, ei saa ridu enam lohistades oma järjekorda seada.',
// ── Screen: trip-costs ────────────────────────────────────────────────────────────────
'help.ctx.trip-costs.title': 'Kulud',
+5 -5
View File
@@ -3092,7 +3092,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Appliquer un modèle et Enregistrer comme modèle remplissent ou conservent une liste sans la retaper, et les deux icônes à côté exportent la liste, en impression, en PDF ou en fichier, et en importent une. Le bouton rouge près de la barre de progression indique combien d’articles sont cochés et les efface.',
'help.ctx.trip-lists.bullet.6':
'À faire a sa propre barre latérale : la carte de progression, les filtres Tout, Mes tâches, En retard et Terminé, Trier par Priorité ou Date d’échéance, et une ligne par liste. Un clic sur une tâche l’ouvre dans le panneau de droite, et Nouvelle tâche ouvre le formulaire Nouvelle tâche au milieu de l’écran.',
'À faire a sa propre barre latérale : la carte de progression, les filtres Tout, Mes tâches, En retard et Terminé, une ligne par liste et Ajouter une liste en dessous. Les tâches sont dans une carte dont l’en-tête nomme le filtre et porte le tri, Priorité ou Date d’échéance. Un clic sur une tâche l’ouvre dans le panneau de droite, et Nouvelle tâche ouvre le formulaire Nouvelle tâche au milieu de l’écran.',
// packing-categories
'help.guide.packing-categories.title': 'Construire la liste de bagages',
'help.guide.packing-categories.goal':
@@ -3241,7 +3241,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Nommez-la dans Nom de la tâche, et mettez tout ce qui vaut la peine d’être retenu sous Description.',
'help.guide.create-todo.step.3':
'Liste groupe la tâche. Choisissez-en une, ou utilisez le signe plus à côté pour taper un nouveau nom.',
'Liste groupe la tâche. Choisissez-en une, ou utilisez le signe plus à côté pour en nommer une nouvelle dans une petite fenêtre.',
'help.guide.create-todo.step.4': 'Priorité, ce sont quatre boutons : Aucune, P1, P2 et P3, du rouge au bleu.',
'help.guide.create-todo.step.5': 'Date d’échéance ouvre un calendrier, et Assigné à met un nom sur la tâche.',
'help.guide.create-todo.step.6': 'Cliquez sur Créer la tâche.',
@@ -3262,16 +3262,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Sous Listes se trouve une ligne par liste. En choisir une montre cette liste, tâches terminées comprises.',
'help.guide.todo-filters.step.3':
'Trier par réordonne ce qui est à l’écran : Priorité met P1 en premier, Date d’échéance met l’échéance la plus proche en premier. Un seul des deux à la fois.',
'Le tri dans l’en-tête de la liste réordonne ce qui est à l’écran : Priorité met P1 en premier, Date d’échéance met l’échéance la plus proche en premier. Un seul des deux à la fois, et un second clic revient à votre propre ordre.',
'help.guide.todo-filters.step.4': 'Cliquez sur une tâche pour l’ouvrir dans le panneau de droite.',
'help.guide.todo-filters.step.5':
'Changez ce qu’il vous faut, Description, Priorité, Liste, Date d’échéance ou Assigné à, puis Enregistrer les modifications. Supprimer emporte la tâche aussitôt.',
'Changez ce qu’il vous faut, Description, Priorité, Liste, Date d’échéance ou Assigné à, puis Enregistrer les modifications. La case dans l’en-tête du panneau coche la tâche, et Supprimer l’emporte aussitôt.',
'help.guide.todo-filters.result':
'La liste ne montre que les tâches que vous avez demandées, et le panneau de droite modifie celle que vous avez choisie.',
'help.guide.todo-filters.tip.1':
'Une ligne de liste ne compte que ce qui reste ouvert, mais la sélectionner montre aussi les tâches terminées. Tout, Mes tâches et En retard cachent ce qui est fait ; Terminé ne montre rien d’autre.',
'help.guide.todo-filters.tip.2':
'Trier par Priorité et Trier par Date d’échéance s’excluent, et tant que l’un des deux est actif, les lignes ne peuvent plus être glissées dans un ordre à vous.',
'Priorité et Date d’échéance dans le tri s’excluent, et tant que l’un des deux est actif, les lignes ne peuvent plus être glissées dans un ordre à vous.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Réservations',
+5 -5
View File
@@ -3101,7 +3101,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Η Εφαρμογή προτύπου και η Αποθήκευση ως πρότυπο γεμίζουν ή κρατούν μια λίστα χωρίς να τη γράψετε, και τα δύο εικονίδια δίπλα τους εξάγουν τη λίστα, ως εκτύπωση, ως PDF ή ως αρχείο, και εισάγουν μία. Το κόκκινο κουμπί δίπλα στη μπάρα προόδου λέει πόσα αντικείμενα είναι επιλεγμένα και τα καθαρίζει.',
'help.ctx.trip-lists.bullet.6':
'Οι Εργασίες έχουν δική τους πλαϊνή στήλη: την κάρτα προόδου, τα φίλτρα Όλα, Οι Εργασίες μου, Εκπρόθεσμα και Ολοκληρωμένα, την Ταξινόμηση κατά Προτεραιότητα ή Ημερομηνία λήξης, και μια σειρά ανά λίστα. Ένα κλικ σε μια εργασία την ανοίγει στο παράθυρο δεξιά, και η Προσθήκη νέας εργασίας ανοίγει τη φόρμα Νέα εργασία πάνω από το μέσο της οθόνης.',
'Οι Εργασίες έχουν δική τους πλαϊνή στήλη: την κάρτα προόδου, τα φίλτρα Όλα, Οι Εργασίες μου, Εκπρόθεσμα και Ολοκληρωμένα, μια σειρά ανά λίστα και κάτω από αυτές την Προσθήκη λίστας. Οι εργασίες βρίσκονται σε μια κάρτα της οποίας η κεφαλίδα γράφει το φίλτρο και κρατά την ταξινόμηση, Προτεραιότητα ή Ημερομηνία λήξης. Ένα κλικ σε μια εργασία την ανοίγει στο παράθυρο δεξιά, και η Προσθήκη νέας εργασίας ανοίγει τη φόρμα Νέα εργασία πάνω από το μέσο της οθόνης.',
// packing-categories
'help.guide.packing-categories.title': 'Χτίστε τη λίστα αποσκευών',
'help.guide.packing-categories.goal':
@@ -3252,7 +3252,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Ονομάστε την στο Όνομα εργασίας και βάλτε ό,τι αξίζει να θυμάστε κάτω από την Περιγραφή.',
'help.guide.create-todo.step.3':
'Η Λίστα ομαδοποιεί την εργασία. Διαλέξτε μία ή χρησιμοποιήστε το συν δίπλα της για να γράψετε νέο όνομα.',
'Η Λίστα ομαδοποιεί την εργασία. Διαλέξτε μία ή χρησιμοποιήστε το συν δίπλα της για να δώσετε όνομα σε μια νέα σε ένα μικρό παράθυρο διαλόγου.',
'help.guide.create-todo.step.4':
'Η Προτεραιότητα είναι τέσσερα κουμπιά: Καμία, P1, P2 και P3, από το κόκκινο ως το μπλε.',
'help.guide.create-todo.step.5':
@@ -3275,16 +3275,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Κάτω από τις Λίστες κάθεται μια σειρά ανά λίστα. Διαλέγοντας μία, βλέπετε εκείνη τη λίστα, μαζί με τις ολοκληρωμένες εργασίες.',
'help.guide.todo-filters.step.3':
'Η Ταξινόμηση κατά αναδιατάσσει ό,τι είναι στην οθόνη: η Προτεραιότητα βάζει πρώτα τα P1, η Ημερομηνία λήξης βάζει πρώτη την κοντινότερη προθεσμία. Μόνο ένα από τα δύο κάθε φορά.',
'Η ταξινόμηση στην κεφαλίδα της λίστας αναδιατάσσει ό,τι είναι στην οθόνη: η Προτεραιότητα βάζει πρώτα τα P1, η Ημερομηνία λήξης βάζει πρώτη την κοντινότερη προθεσμία. Μόνο ένα από τα δύο κάθε φορά, και ένα δεύτερο κλικ επιστρέφει στη δική σας σειρά.',
'help.guide.todo-filters.step.4': 'Κάντε κλικ σε μια εργασία για να ανοίξει στο παράθυρο δεξιά.',
'help.guide.todo-filters.step.5':
'Αλλάξτε ό,τι χρειάζεστε, Περιγραφή, Προτεραιότητα, Λίστα, Ημερομηνία λήξης ή Ανατεθειμένο σε, και μετά Αποθήκευση αλλαγών. Η Διαγραφή παίρνει την εργασία αμέσως.',
'Αλλάξτε ό,τι χρειάζεστε, Περιγραφή, Προτεραιότητα, Λίστα, Ημερομηνία λήξης ή Ανατεθειμένο σε, και μετά Αποθήκευση αλλαγών. Το κουτάκι στην κεφαλίδα του παραθύρου σημειώνει την εργασία ως ολοκληρωμένη, και η Διαγραφή την παίρνει αμέσως.',
'help.guide.todo-filters.result':
'Η λίστα δείχνει μόνο τις εργασίες που ζητήσατε, και το παράθυρο δεξιά επεξεργάζεται αυτή που διαλέξατε.',
'help.guide.todo-filters.tip.1':
'Μια σειρά λίστας μετρά μόνο ό,τι είναι ακόμη ανοιχτό, αλλά αν την επιλέξετε δείχνει και τις ολοκληρωμένες εργασίες. Τα Όλα, οι Οι Εργασίες μου και τα Εκπρόθεσμα κρύβουν ό,τι έχει γίνει· τα Ολοκληρωμένα δεν δείχνουν τίποτα άλλο.',
'help.guide.todo-filters.tip.2':
'Η Ταξινόμηση κατά Προτεραιότητα και η Ταξινόμηση κατά Ημερομηνία λήξης αποκλείουν η μία την άλλη, και όσο κάποια είναι ενεργή οι σειρές δεν σύρονται πια σε δική σας σειρά.',
'Η Προτεραιότητα και η Ημερομηνία λήξης στην ταξινόμηση αποκλείουν η μία την άλλη, και όσο κάποια είναι ενεργή οι σειρές δεν σύρονται πια σε δική σας σειρά.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Κρατήσεις',
+5 -5
View File
@@ -3030,7 +3030,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'A Sablon alkalmazása és a Mentés sablonként gépelés nélkül tölti fel vagy őrzi meg a listát, a mellettük lévő két ikon pedig exportálja a listát, nyomtatásként, PDF-ként vagy fájlként, illetve importál egyet. A haladási sáv melletti piros gomb megmondja, hány tétel van kipipálva, és eltakarítja őket.',
'help.ctx.trip-lists.bullet.6':
'A Teendőknek saját oldalsávja van: a haladási kártya, a Mind, a Saját feladataim, a Lejárt és a Kész szűrők, a Rendezés Prioritás vagy Határidő szerint, és listánként egy sor. Egy feladatra kattintva a jobb oldali panelen nyílik meg, az Új feladat pedig a képernyő közepén nyitja meg az Új feladat űrlapot.',
'A Teendőknek saját oldalsávja van: a haladási kártya, a Mind, a Saját feladataim, a Lejárt és a Kész szűrők, listánként egy sor, alattuk pedig a Lista hozzáadása. A feladatok egy kártyán ülnek, amelynek fejléce megnevezi a szűrőt és a rendezést is hordozza, Prioritás vagy Határidő. Egy feladatra kattintva a jobb oldali panelen nyílik meg, az Új feladat pedig a képernyő közepén nyitja meg az Új feladat űrlapot.',
// packing-categories
'help.guide.packing-categories.title': 'Csomagolási lista felépítése',
'help.guide.packing-categories.goal':
@@ -3178,7 +3178,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Nevezd el a Feladat neve mezőben, és tedd a Leírás alá mindazt, amit érdemes megjegyezni.',
'help.guide.create-todo.step.3':
'A Lista csoportosítja a feladatot. Válassz egyet, vagy használd a mellette lévő pluszt új név beírásához.',
'A Lista csoportosítja a feladatot. Válassz egyet, vagy a mellette lévő plusszal nevezz el egy újat egy kis párbeszédablakban.',
'help.guide.create-todo.step.4': 'A Prioritás négy gomb: Nincs, P1, P2 és P3, a pirostól a kékig.',
'help.guide.create-todo.step.5': 'A Határidő naptárat nyit, a Hozzárendelve pedig nevet tesz a feladatra.',
'help.guide.create-todo.step.6': 'Kattints a Feladat létrehozása gombra.',
@@ -3197,16 +3197,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'A Listák alatt listánként egy sor ül. Ha kiválasztasz egyet, azt a listát mutatja, a befejezett feladatokkal együtt.',
'help.guide.todo-filters.step.3':
'A Rendezés átrendezi, ami a képernyőn van: a Prioritás a P1 elemeket teszi előre, a Határidő a legközelebbi határidőt. Egyszerre csak a kettő egyike.',
'A lista fejlécében lévő rendezés átrendezi, ami a képernyőn van: a Prioritás a P1 elemeket teszi előre, a Határidő a legközelebbi határidőt. Egyszerre csak a kettő egyike, és egy második kattintás visszaáll a saját sorrendedre.',
'help.guide.todo-filters.step.4': 'Kattints egy feladatra, hogy a jobb oldali panelen nyíljon meg.',
'help.guide.todo-filters.step.5':
'Változtasd meg, amit kell, Leírás, Prioritás, Lista, Határidő vagy Hozzárendelve, aztán Módosítások mentése. A Törlés azonnal elviszi a feladatot.',
'Változtasd meg, amit kell, Leírás, Prioritás, Lista, Határidő vagy Hozzárendelve, aztán Módosítások mentése. A panel fejlécében lévő jelölőnégyzet kipipálja a feladatot, a Törlés pedig azonnal elviszi.',
'help.guide.todo-filters.result':
'A lista csak azokat a feladatokat mutatja, amiket kértél, a jobb oldali panel pedig azt szerkeszti, amit kiválasztottál.',
'help.guide.todo-filters.tip.1':
'Egy listasor csak azt számolja, ami még nyitott, de ha kiválasztod, a befejezett feladatokat is mutatja. A Mind, a Saját feladataim és a Lejárt elrejti, ami kész; a Kész mást nem mutat.',
'help.guide.todo-filters.tip.2':
'A Rendezés Prioritás szerint és a Rendezés Határidő szerint kizárja egymást, és amíg bármelyik be van kapcsolva, a sorokat már nem lehet saját sorrendbe húzni.',
'A rendezésben a Prioritás és a Határidő kizárja egymást, és amíg bármelyik be van kapcsolva, a sorokat már nem lehet saját sorrendbe húzni.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Foglalások',
+5 -5
View File
@@ -3035,7 +3035,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'“Terapkan template” dan “Simpan sebagai template” mengisi atau menyimpan sebuah daftar tanpa mengetiknya, dan dua ikon di sebelahnya mengekspor daftar, sebagai cetakan, PDF, atau file, dan mengimpor daftar. Tombol merah di sebelah bilah kemajuan menyebut berapa item yang tercentang lalu menyingkirkannya.',
'help.ctx.trip-lists.bullet.6':
'“Tugas” punya bilah sisi sendiri: kartu kemajuan, filter “Semua”, “Tugasku”, “Terlambat” dan “Selesai”, “Urutkan” menurut “Prioritas” atau “Tenggat waktu”, dan satu baris per daftar. Klik pada sebuah tugas membukanya di panel sebelah kanan, dan “Tugas baru” membuka formulir “Tugas baru” di atas bagian tengah layar.',
'“Tugas” punya bilah sisi sendiri: kartu kemajuan, filter “Semua”, “Tugasku”, “Terlambat” dan “Selesai”, satu baris per daftar dan “Tambah daftar” di bawahnya. Tugas-tugas berada di sebuah kartu yang kepalanya menyebut nama filter dan memuat pengurutan, “Prioritas” atau “Tenggat waktu”. Klik pada sebuah tugas membukanya di panel sebelah kanan, dan “Tugas baru” membuka formulir “Tugas baru” di atas bagian tengah layar.',
// packing-categories
'help.guide.packing-categories.title': 'Membangun daftar bawaan',
'help.guide.packing-categories.goal':
@@ -3183,7 +3183,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Beri nama di “Nama tugas”, dan taruh apa pun yang layak diingat di bawah “Deskripsi”.',
'help.guide.create-todo.step.3':
'“Daftar” mengelompokkan tugas. Pilih satu, atau gunakan tanda plus di sebelahnya untuk mengetik nama baru.',
'“Daftar” mengelompokkan tugas. Pilih satu, atau gunakan tanda plus di sebelahnya untuk menamai daftar baru di sebuah dialog kecil.',
'help.guide.create-todo.step.4':
'“Prioritas” adalah empat tombol: “Tidak ada”, P1, P2 dan P3, dari merah turun ke biru.',
'help.guide.create-todo.step.5':
@@ -3204,16 +3204,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Di bawah “Daftar” duduk satu baris per daftar. Memilih salah satunya menampilkan daftar itu, termasuk tugas yang sudah selesai.',
'help.guide.todo-filters.step.3':
'“Urutkan” menata ulang apa yang ada di layar: “Prioritas” menaruh P1 di depan, “Tenggat waktu” menaruh tenggat terdekat di depan. Hanya satu dari keduanya pada satu waktu.',
'Pengurutan di kepala daftar menata ulang apa yang ada di layar: “Prioritas” menaruh P1 di depan, “Tenggat waktu” menaruh tenggat terdekat di depan. Hanya satu dari keduanya pada satu waktu, dan klik kedua mengembalikan urutan Anda sendiri.',
'help.guide.todo-filters.step.4': 'Klik sebuah tugas untuk membukanya di panel sebelah kanan.',
'help.guide.todo-filters.step.5':
'Ubah apa yang perlu, “Deskripsi”, “Prioritas”, “Daftar”, “Tenggat waktu” atau “Ditugaskan ke”, lalu “Simpan perubahan”. “Hapus” menyingkirkan tugas itu seketika.',
'Ubah apa yang perlu, “Deskripsi”, “Prioritas”, “Daftar”, “Tenggat waktu” atau “Ditugaskan ke”, lalu “Simpan perubahan”. Kotak di kepala panel menandai tugas sebagai selesai, dan “Hapus” menyingkirkannya seketika.',
'help.guide.todo-filters.result':
'Daftar hanya menampilkan tugas yang Anda minta, dan panel di sebelah kanan menyunting yang Anda pilih.',
'help.guide.todo-filters.tip.1':
'Baris daftar hanya menghitung yang masih terbuka, tetapi memilihnya menampilkan tugas yang sudah selesai juga. “Semua”, “Tugasku” dan “Terlambat” menyembunyikan yang sudah rampung; “Selesai” tidak menampilkan yang lain.',
'help.guide.todo-filters.tip.2':
'“Urutkan” menurut “Prioritas” dan “Urutkan” menurut “Tenggat waktu” saling meniadakan, dan selama salah satunya menyala baris tidak bisa lagi diseret ke urutan Anda sendiri.',
'“Prioritas” dan “Tenggat waktu” dalam pengurutan saling meniadakan, dan selama salah satunya menyala baris tidak bisa lagi diseret ke urutan Anda sendiri.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Pemesanan',
+5 -5
View File
@@ -3016,7 +3016,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Applica modello e Salva come modello riempiono o conservano una lista senza digitarla, e le due icone accanto esportano la lista, come stampa, come PDF o come file, e ne importano una. Il pulsante rosso vicino alla barra di avanzamento dice quanti elementi sono spuntati e li porta via.',
'help.ctx.trip-lists.bullet.6':
'Da fare ha una barra laterale propria: la scheda di avanzamento, i filtri Tutti, Le mie attività, Scaduta e Fatto, Ordina per Priorità o Scadenza, e una riga per lista. Un clic su un’attività la apre nel pannello a destra, e Nuova attività apre il modulo Nuova attività sopra il centro dello schermo.',
'Da fare ha una barra laterale propria: la scheda di avanzamento, i filtri Tutti, Le mie attività, Scaduta e Fatto, una riga per lista e sotto Aggiungi lista. Le attività stanno in una scheda la cui intestazione nomina il filtro e contiene l’ordinamento, Priorità o Scadenza. Un clic su un’attività la apre nel pannello a destra, e Nuova attività apre il modulo Nuova attività sopra il centro dello schermo.',
// packing-categories
'help.guide.packing-categories.title': 'Costruire la lista valigia',
'help.guide.packing-categories.goal':
@@ -3163,7 +3163,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Dalle un nome in Nome attività, e metti sotto Descrizione tutto quello che vale la pena ricordare.',
'help.guide.create-todo.step.3':
'Lista raggruppa l’attività. Scegline una, oppure usa il segno più accanto per digitare un nome nuovo.',
'Lista raggruppa l’attività. Scegline una, oppure usa il segno più accanto per dare un nome a una nuova in una piccola finestra.',
'help.guide.create-todo.step.4': 'Priorità sono quattro pulsanti: Nessuna, P1, P2 e P3, dal rosso al blu.',
'help.guide.create-todo.step.5': 'Scadenza apre un calendario, e Assegnato a mette un nome sull’attività.',
'help.guide.create-todo.step.6': 'Clicca Crea attività.',
@@ -3183,16 +3183,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Sotto Liste sta una riga per lista. Sceglierne una mostra quella lista, attività finite comprese.',
'help.guide.todo-filters.step.3':
'Ordina per riordina quello che è sullo schermo: Priorità mette P1 per primo, Scadenza mette la scadenza più vicina per prima. Solo uno dei due alla volta.',
'L’ordinamento nell’intestazione della lista riordina quello che è sullo schermo: Priorità mette P1 per primo, Scadenza mette la scadenza più vicina per prima. Solo uno dei due alla volta, e un secondo clic torna al tuo ordine.',
'help.guide.todo-filters.step.4': 'Clicca un’attività per aprirla nel pannello a destra.',
'help.guide.todo-filters.step.5':
'Cambia quello che ti serve, Descrizione, Priorità, Lista, Scadenza o Assegnato a, poi Salva modifiche. Elimina porta via l’attività all’istante.',
'Cambia quello che ti serve, Descrizione, Priorità, Lista, Scadenza o Assegnato a, poi Salva modifiche. La casella nell’intestazione del pannello spunta l’attività, ed Elimina la porta via all’istante.',
'help.guide.todo-filters.result':
'La lista mostra solo le attività che hai chiesto, e il pannello a destra modifica quella che hai scelto.',
'help.guide.todo-filters.tip.1':
'Una riga di lista conta solo quello che è ancora aperto, ma selezionarla mostra anche le attività finite. Tutti, Le mie attività e Scaduta nascondono quello che è fatto; Fatto non mostra altro.',
'help.guide.todo-filters.tip.2':
'Ordina per Priorità e Ordina per Scadenza si escludono, e finché uno dei due è attivo le righe non si possono più trascinare in un ordine tuo.',
'Priorità e Scadenza nell’ordinamento si escludono, e finché uno dei due è attivo le righe non si possono più trascinare in un ordine tuo.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Prenotazioni',
+5 -5
View File
@@ -2892,7 +2892,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'「テンプレートを適用」と「テンプレートとして保存」は、打ち込まずにリストを埋めたり残したりします。その隣の2つのアイコンは、リストを印刷物、PDF、ファイルとしてエクスポートし、リストをインポートします。進捗バーの横の赤いボタンはチェック済みのアイテムが何件あるかを出し、まとめて片づけます。',
'help.ctx.trip-lists.bullet.6':
'「ToDo」には専用のサイドバーがあります。進捗カード、「すべて」「自分のタスク」「期限切れ」「完了」のフィルター、「優先度」か「期限」での「並び替え」、そしてリストごとに1行です。タスクをクリックすると右のパネルで開き、「新しいタスクを追加...」は画面の中央に「新しいタスク」のフォームを開きます。',
'「ToDo」には専用のサイドバーがあります。進捗カード、「すべて」「自分のタスク」「期限切れ」「完了」のフィルター、リストごとに1行、その下に「リストを追加」です。タスクはカードの中に並び、カードの見出しにはフィルター名が出て、「優先度」か「期限」の並び替えもそこにあります。タスクをクリックすると右のパネルで開き、「新しいタスクを追加...」は画面の中央に「新しいタスク」のフォームを開きます。',
// packing-categories
'help.guide.packing-categories.title': '持ち物リストを作る',
'help.guide.packing-categories.goal':
@@ -3029,7 +3029,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.1': '右上の「新しいタスクを追加...」をクリックします。',
'help.guide.create-todo.step.2': '「タスク名」で名前を付け、覚えておきたいことは「説明」に入れます。',
'help.guide.create-todo.step.3':
'「リスト」がタスクをまとめます。1つ選ぶか、その隣のプラスで新しい名前を入力します。',
'「リスト」がタスクをまとめます。1つ選ぶか、その隣のプラスを使って小さなダイアログで新しいリストに名前を付けます。',
'help.guide.create-todo.step.4': '「優先度」は4つのボタンです。「なし」、P1、P2、P3で、赤から青へと下がります。',
'help.guide.create-todo.step.5': '「期限」はカレンダーを開き、「担当者」はタスクに名前を付けます。',
'help.guide.create-todo.step.6': '「タスクを作成」をクリックします。',
@@ -3046,15 +3046,15 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'「リスト」の下にはリストごとに1行あります。1つ選ぶと、そのリストが、終わったタスクも含めて出ます。',
'help.guide.todo-filters.step.3':
'「並び替え」は画面にあるものを並べ替えます。「優先度」はP1を先に、「期限」はいちばん近い締め切りを先にします。2つは一度に片方だけです。',
'リストの見出しにある並び替えは、画面にあるものを並べ替えます。「優先度」はP1を先に、「期限」はいちばん近い締め切りを先にします。2つは一度に片方だけで、もう一度クリックすると自分の順に戻ります。',
'help.guide.todo-filters.step.4': 'タスクをクリックすると、右のパネルで開きます。',
'help.guide.todo-filters.step.5':
'必要なところを変えます。「説明」「優先度」「リスト」「期限」「担当者」です。それから「変更を保存」。「削除」はタスクをその場で消します。',
'必要なところを変えます。「説明」「優先度」「リスト」「期限」「担当者」です。それから「変更を保存」。パネルの見出しにあるチェックボックスでタスクを完了にでき、「削除」はその場で消します。',
'help.guide.todo-filters.result': '一覧には求めたタスクだけが出て、右のパネルが選んだ1つを編集します。',
'help.guide.todo-filters.tip.1':
'リストの行が数えるのはまだ未完了のものだけですが、選ぶと終わったタスクも出ます。「すべて」「自分のタスク」「期限切れ」は終わったものを隠し、「完了」はそれしか出しません。',
'help.guide.todo-filters.tip.2':
'「並び替え」の「優先度」と「期限」は互いを排除し、どちらかが効いている間は、行を自分の順に並べ替えることはできません。',
'並び替えの「優先度」と「期限」は互いを排除し、どちらかが効いている間は、行を自分の順に並べ替えることはできません。',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': '予約',
+6 -5
View File
@@ -2793,7 +2793,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'“템플릿 적용”과 “템플릿으로 저장”은 일일이 입력하지 않고도 목록을 채우거나 남겨 둡니다. 그 옆의 아이콘 두 개는 목록을 인쇄물, PDF, 파일로 내보내고, 목록을 가져옵니다. 진행 막대 옆의 빨간 버튼은 체크된 항목이 몇 개인지 말하고 그것들을 치웁니다.',
'help.ctx.trip-lists.bullet.6':
'“할 일”에는 전용 사이드바가 있습니다. 진행 카드, “전체”, “내 작업”, “기한 초과”, “완료” 필터, “우선순위”나 “마감일”로 하는 “정렬 기준”, 그리고 목록마다 한 행입니다. 작업을 클릭하면 오른쪽 패널에서 열리고, “새 작업 추가”는 화면 가운데에 “새 작업” 양식을 엽니다.',
'“할 일”에는 전용 사이드바가 있습니다. 진행 카드, “전체”, “내 작업”, “기한 초과”, “완료” 필터, 목록마다 한 행, 그리고 그 아래의 “목록 추가”입니다. 작업은 카드 안에 있으며, 카드 머리글에는 필터 이름이 표시되고 “우선순위”나 “마감일”로 하는 정렬도 여기에 있습니다. 작업을 클릭하면 오른쪽 패널에서 열리고, “새 작업 추가”는 화면 가운데에 “새 작업” 양식을 엽니다.',
// packing-categories
'help.guide.packing-categories.title': '짐 목록 만들기',
'help.guide.packing-categories.goal': '가져갈 것을 목록으로 묶고, 항목을 채우고, 각 목록을 누가 맡는지 정합니다.',
@@ -2924,7 +2924,8 @@ const help: TranslationStrings = {
'help.guide.create-todo.goal': '일어나야 할 일을 목록, 우선순위, 날짜, 그리고 담당자와 함께 적어 둡니다.',
'help.guide.create-todo.step.1': '오른쪽 위의 “새 작업 추가”를 클릭합니다.',
'help.guide.create-todo.step.2': '“작업 이름”에 이름을 붙이고, 기억해 둘 만한 것은 “설명”에 적습니다.',
'help.guide.create-todo.step.3': '“목록”이 작업을 묶습니다. 하나를 고르거나, 옆의 더하기로 새 이름을 입력합니다.',
'help.guide.create-todo.step.3':
'“목록”이 작업을 묶습니다. 하나를 고르거나, 옆의 더하기를 눌러 작은 대화 상자에서 새 목록의 이름을 정합니다.',
'help.guide.create-todo.step.4':
'“우선순위”는 버튼 네 개입니다. “없음”, P1, P2, P3이고 빨강에서 파랑으로 내려갑니다.',
'help.guide.create-todo.step.5': '“마감일”은 달력을 열고, “배정 대상”은 작업에 이름을 붙입니다.',
@@ -2942,15 +2943,15 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'“목록” 아래에는 목록마다 한 행이 있습니다. 하나를 고르면 끝난 작업까지 포함해 그 목록이 보입니다.',
'help.guide.todo-filters.step.3':
'“정렬 기준”은 화면에 있는 것을 다시 늘어놓습니다. “우선순위”는 P1을 앞에, “마감일”은 가장 가까운 기한을 앞에 둡니다. 둘 중 하나만 한 번에 됩니다.',
'목록 머리글에 있는 정렬은 화면에 있는 것을 다시 늘어놓습니다. “우선순위”는 P1을 앞에, “마감일”은 가장 가까운 기한을 앞에 둡니다. 둘 중 하나만 한 번에 되며, 한 번 더 클릭하면 내가 정한 순서로 돌아갑니다.',
'help.guide.todo-filters.step.4': '작업을 클릭해 오른쪽 패널에서 엽니다.',
'help.guide.todo-filters.step.5':
'필요한 것을 바꿉니다. “설명”, “우선순위”, “목록”, “마감일”, “배정 대상”입니다. 그다음 “변경 사항 저장”입니다. “삭제”는 작업을 즉시 없앱니다.',
'필요한 것을 바꿉니다. “설명”, “우선순위”, “목록”, “마감일”, “배정 대상”입니다. 그다음 “변경 사항 저장”입니다. 패널 머리글의 체크 상자는 작업을 완료로 표시하고, “삭제”는 작업을 즉시 없앱니다.',
'help.guide.todo-filters.result': '목록에는 요청한 작업만 보이고, 오른쪽 패널이 고른 작업을 편집합니다.',
'help.guide.todo-filters.tip.1':
'목록 행은 아직 열려 있는 것만 세지만, 선택하면 끝난 작업도 보입니다. “전체”, “내 작업”, “기한 초과”는 끝난 것을 감추고, “완료”는 그것만 보여 줍니다.',
'help.guide.todo-filters.tip.2':
'“정렬 기준” “우선순위”와 “정렬 기준” “마감일”은 서로를 배제하며, 둘 중 하나가 켜져 있는 동안에는 행을 내가 정한 순서로 끌어 옮길 수 없습니다.',
'정렬의 “우선순위”와 “마감일”은 서로를 배제하며, 둘 중 하나가 켜져 있는 동안에는 행을 내가 정한 순서로 끌어 옮길 수 없습니다.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': '예약',
+5 -5
View File
@@ -3036,7 +3036,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Sjabloon toepassen en Opslaan als sjabloon vullen of bewaren een lijst zonder hem uit te typen, en de twee pictogrammen ernaast exporteren de lijst, als afdruk, als PDF of als bestand, en importeren er een. De rode knop naast de voortgangsbalk noemt hoeveel items aangevinkt zijn en ruimt ze op.',
'help.ctx.trip-lists.bullet.6':
'Taken heeft een eigen zijbalk: de voortgangskaart, de filters Alles, Mijn taken, Verlopen en Klaar, Sorteren op Prioriteit of Vervaldatum, en één rij per lijst. Een klik op een taak opent hem in het paneel rechts, en Nieuwe taak opent het formulier Nieuwe taak over het midden van het scherm.',
'Taken heeft een eigen zijbalk: de voortgangskaart, de filters Alles, Mijn taken, Verlopen en Klaar, één rij per lijst en daaronder Lijst toevoegen. De taken staan in een kaart waarvan de kop het filter noemt en de sortering bevat, Prioriteit of Vervaldatum. Een klik op een taak opent hem in het paneel rechts, en Nieuwe taak opent het formulier Nieuwe taak over het midden van het scherm.',
// packing-categories
'help.guide.packing-categories.title': 'De inpaklijst opbouwen',
'help.guide.packing-categories.goal':
@@ -3181,7 +3181,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Geef hem een naam in Taaknaam, en zet alles wat het onthouden waard is onder Beschrijving.',
'help.guide.create-todo.step.3':
'Lijst groepeert de taak. Kies er een, of gebruik de plus ernaast om een nieuwe naam te typen.',
'Lijst groepeert de taak. Kies er een, of gebruik de plus ernaast om een nieuwe te benoemen in een klein dialoogvenster.',
'help.guide.create-todo.step.4': 'Prioriteit is vier knoppen: Geen, P1, P2 en P3, van rood tot blauw.',
'help.guide.create-todo.step.5': 'Vervaldatum opent een kalender, en Toegewezen aan zet een naam op de taak.',
'help.guide.create-todo.step.6': 'Klik op Taak aanmaken.',
@@ -3201,16 +3201,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Onder Lijsten staat één rij per lijst. Er een kiezen toont die lijst, afgeronde taken inbegrepen.',
'help.guide.todo-filters.step.3':
'Sorteren op herschikt wat op het scherm staat: Prioriteit zet P1 vooraan, Vervaldatum zet de dichtstbijzijnde deadline vooraan. Maar één van de twee tegelijk.',
'De sortering in de kop van de lijst herschikt wat op het scherm staat: Prioriteit zet P1 vooraan, Vervaldatum zet de dichtstbijzijnde deadline vooraan. Maar één van de twee tegelijk, en een tweede klik gaat terug naar je eigen volgorde.',
'help.guide.todo-filters.step.4': 'Klik op een taak om hem in het paneel rechts te openen.',
'help.guide.todo-filters.step.5':
'Verander wat je nodig hebt, Beschrijving, Prioriteit, Lijst, Vervaldatum of Toegewezen aan, en dan Wijzigingen opslaan. Verwijderen haalt de taak meteen weg.',
'Verander wat je nodig hebt, Beschrijving, Prioriteit, Lijst, Vervaldatum of Toegewezen aan, en dan Wijzigingen opslaan. Het vakje in de kop van het paneel vinkt de taak af, en Verwijderen haalt hem meteen weg.',
'help.guide.todo-filters.result':
'De lijst toont alleen de taken waar je om vroeg, en het paneel rechts bewerkt degene die je koos.',
'help.guide.todo-filters.tip.1':
'Een lijstrij telt alleen wat nog open staat, maar hem selecteren toont ook de afgeronde taken. Alles, Mijn taken en Verlopen verbergen wat af is; Klaar toont niets anders.',
'help.guide.todo-filters.tip.2':
'Sorteren op Prioriteit en Sorteren op Vervaldatum sluiten elkaar uit, en zolang een van beide aan staat kunnen de rijen niet meer in een eigen volgorde gesleept worden.',
'Prioriteit en Vervaldatum in de sortering sluiten elkaar uit, en zolang een van beide aan staat kunnen de rijen niet meer in een eigen volgorde gesleept worden.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Boekingen',
+6 -5
View File
@@ -2964,7 +2964,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Zastosuj szablon i Zapisz jako szablon wypełniają albo zachowują listę bez pisania jej od nowa, a dwie ikony obok nich eksportują listę, jako wydruk, PDF albo plik, i importują listę. Czerwony przycisk przy pasku postępu mówi, ile przedmiotów jest zaznaczonych, i sprząta je.',
'help.ctx.trip-lists.bullet.6':
'Do zrobienia ma własny panel boczny: kartę postępu, filtry Wszystkie, Moje zadania, Przeterminowane i Gotowe, Sortuj wg Priorytet albo Termin, oraz jeden wiersz na listę. Kliknięcie zadania otwiera je w panelu po prawej, a Nowe zadanie otwiera na środku ekranu formularz Nowe zadanie.',
'Do zrobienia ma własny panel boczny: kartę postępu, filtry Wszystkie, Moje zadania, Przeterminowane i Gotowe, jeden wiersz na listę i pod nimi Dodaj listę. Zadania leżą w karcie, której nagłówek nazywa filtr i mieści sortowanie, Priorytet albo Termin. Kliknięcie zadania otwiera je w panelu po prawej, a Nowe zadanie otwiera na środku ekranu formularz Nowe zadanie.',
// packing-categories
'help.guide.packing-categories.title': 'Zbudować listę pakowania',
'help.guide.packing-categories.goal':
@@ -3105,7 +3105,8 @@ const help: TranslationStrings = {
'help.guide.create-todo.goal': 'Zapisz coś, co musi się wydarzyć, z listą, priorytetem, datą i imieniem przy tym.',
'help.guide.create-todo.step.1': 'Kliknij Nowe zadanie u góry po prawej.',
'help.guide.create-todo.step.2': 'Nazwij je w Nazwa zadania i wpisz wszystko, co warto zapamiętać, pod Opis.',
'help.guide.create-todo.step.3': 'Lista grupuje zadanie. Wybierz jedną albo użyj plusa obok, żeby wpisać nową nazwę.',
'help.guide.create-todo.step.3':
'Lista grupuje zadanie. Wybierz jedną albo użyj plusa obok, żeby w małym oknie nadać nazwę nowej.',
'help.guide.create-todo.step.4': 'Priorytet to cztery przyciski: Brak, P1, P2 i P3, od czerwonego do niebieskiego.',
'help.guide.create-todo.step.5': 'Termin otwiera kalendarz, a Przypisano do umieszcza na zadaniu imię.',
'help.guide.create-todo.step.6': 'Kliknij Utwórz zadanie.',
@@ -3124,16 +3125,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Pod Listy siedzi jeden wiersz na listę. Wybranie jednego pokazuje tę listę, razem z ukończonymi zadaniami.',
'help.guide.todo-filters.step.3':
'Sortuj wg zmienia kolejność tego, co na ekranie: Priorytet daje na przód P1, Termin daje na przód najbliższy termin. Tylko jedno z dwóch naraz.',
'Sortowanie w nagłówku listy zmienia kolejność tego, co na ekranie: Priorytet daje na przód P1, Termin daje na przód najbliższy termin. Tylko jedno z dwóch naraz, a drugie kliknięcie wraca do twojej własnej kolejności.',
'help.guide.todo-filters.step.4': 'Kliknij zadanie, żeby otworzyć je w panelu po prawej.',
'help.guide.todo-filters.step.5':
'Zmień, co trzeba, Opis, Priorytet, Lista, Termin albo Przypisano do, a potem Zapisz zmiany. Usuń zabiera zadanie od razu.',
'Zmień, co trzeba, Opis, Priorytet, Lista, Termin albo Przypisano do, a potem Zapisz zmiany. Pole wyboru w nagłówku panelu odhacza zadanie, a Usuń zabiera je od razu.',
'help.guide.todo-filters.result':
'Lista pokazuje tylko zadania, o które prosiłeś, a panel po prawej edytuje to, które wybrałeś.',
'help.guide.todo-filters.tip.1':
'Wiersz listy liczy tylko to, co jeszcze otwarte, ale wybranie go pokazuje też ukończone zadania. Wszystkie, Moje zadania i Przeterminowane ukrywają to, co gotowe; Gotowe nie pokazują nic innego.',
'help.guide.todo-filters.tip.2':
'Sortuj wg Priorytet i Sortuj wg Termin wykluczają się nawzajem, a dopóki któreś jest włączone, wierszy nie da się już przeciągać we własną kolejność.',
'Priorytet i Termin w sortowaniu wykluczają się nawzajem, a dopóki któreś jest włączone, wierszy nie da się już przeciągać we własną kolejność.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Rezerwacje',
+5 -5
View File
@@ -2980,7 +2980,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Применить шаблон и Сохранить как шаблон наполняют или сохраняют список без набора вручную, а два значка рядом с ними экспортируют список, как распечатку, PDF или файл, и импортируют список. Красная кнопка рядом с полосой прогресса называет, сколько вещей отмечено, и убирает их.',
'help.ctx.trip-lists.bullet.6':
'У Задачи есть своя боковая панель: карточка прогресса, фильтры Все, Мои задачи, Просроченные и Выполненные, Сортировать по Приоритет или Срок выполнения и по одной строке на список. Нажатие на задачу открывает её в панели справа, а Новая задача открывает посреди экрана форму Новая задача.',
'У Задачи есть своя боковая панель: карточка прогресса, фильтры Все, Мои задачи, Просроченные и Выполненные, по одной строке на список и под ними Добавить список. Задачи лежат в карточке, в заголовке которой указан фильтр и находится сортировка, Приоритет или Срок выполнения. Нажатие на задачу открывает её в панели справа, а Новая задача открывает посреди экрана форму Новая задача.',
// packing-categories
'help.guide.packing-categories.title': 'Собрать список вещей',
'help.guide.packing-categories.goal':
@@ -3121,7 +3121,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.1': 'Нажмите Новая задача справа вверху.',
'help.guide.create-todo.step.2': 'Назовите её в Название задачи и положите всё, что стоит запомнить, под Описание.',
'help.guide.create-todo.step.3':
'Список группирует задачу. Выберите один или используйте плюс рядом, чтобы ввести новое название.',
'Список группирует задачу. Выберите один или используйте плюс рядом, чтобы назвать новый в небольшом диалоге.',
'help.guide.create-todo.step.4': 'Приоритет это четыре кнопки: Нет, P1, P2 и P3, от красного до синего.',
'help.guide.create-todo.step.5': 'Срок выполнения открывает календарь, а Назначено ставит на задачу имя.',
'help.guide.create-todo.step.6': 'Нажмите Создать задачу.',
@@ -3139,16 +3139,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Под Списки сидит по одной строке на список. Выбор одного показывает этот список, вместе с выполненными задачами.',
'help.guide.todo-filters.step.3':
'Сортировать по меняет порядок того, что на экране: Приоритет ставит вперёд P1, Срок выполнения ставит вперёд ближайший срок. Только одно из двух за раз.',
'Сортировка в заголовке списка меняет порядок того, что на экране: Приоритет ставит вперёд P1, Срок выполнения ставит вперёд ближайший срок. Только одно из двух за раз, а повторное нажатие возвращает ваш собственный порядок.',
'help.guide.todo-filters.step.4': 'Нажмите на задачу, чтобы открыть её в панели справа.',
'help.guide.todo-filters.step.5':
'Измените, что нужно, Описание, Приоритет, Список, Срок выполнения или Назначено, а затем Сохранить изменения. Удалить убирает задачу сразу.',
'Измените, что нужно, Описание, Приоритет, Список, Срок выполнения или Назначено, а затем Сохранить изменения. Флажок в заголовке панели отмечает задачу как выполненную, а Удалить убирает её сразу.',
'help.guide.todo-filters.result':
'Список показывает только те задачи, о которых вы попросили, а панель справа редактирует ту, которую вы выбрали.',
'help.guide.todo-filters.tip.1':
'Строка списка считает только то, что ещё открыто, но её выбор показывает и выполненные задачи. Все, Мои задачи и Просроченные скрывают то, что сделано; Выполненные не показывают ничего другого.',
'help.guide.todo-filters.tip.2':
'Сортировать по Приоритет и Сортировать по Срок выполнения исключают друг друга, и пока одно из них включено, строки уже нельзя перетаскивать в свой собственный порядок.',
'Приоритет и Срок выполнения в сортировке исключают друг друга, и пока одно из них включено, строки уже нельзя перетаскивать в свой собственный порядок.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Бронирования',
+5 -5
View File
@@ -3057,7 +3057,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Použiť šablónu a Uložiť ako šablónu naplnia alebo uchovajú zoznam bez vypisovania a dve ikony vedľa nich zoznam exportujú, ako výtlačok, PDF alebo súbor, alebo nejaký importujú. Červené tlačidlo pri lište priebehu uvádza, koľko položiek je zaškrtnutých, a odstráni ich.',
'help.ctx.trip-lists.bullet.6':
'Úlohy majú vlastný bočný panel: kartu priebehu, filtre Všetky, Moje úlohy, Po termíne a Hotovo, Zoradiť podľa Priorita alebo Termín splnenia a jeden riadok na zoznam. Kliknutím na úlohu ju otvoríte v paneli vpravo a Pridať novú úlohu otvorí formulár Nová úloha uprostred obrazovky.',
'Úlohy majú vlastný bočný panel: kartu priebehu, filtre Všetko, Moje úlohy, Po termíne a Hotové, jeden riadok na zoznam a pod nimi Pridať zoznam. Úlohy sú v karte, ktorej hlavička pomenúva filter a nesie zoradenie, Priorita alebo Termín splnenia. Kliknutím na úlohu ju otvoríte v paneli vpravo a Pridať novú úlohu otvorí formulár Nová úloha uprostred obrazovky.',
// packing-categories
'help.guide.packing-categories.title': 'Zostavte zoznam vecí',
'help.guide.packing-categories.goal':
@@ -3197,7 +3197,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Pomenujte ju v poli Názov úlohy a všetko, čo stojí za zapamätanie, napíšte do poľa Popis.',
'help.guide.create-todo.step.3':
'Zoznam úlohy zoskupuje. Vyberte jeden alebo pomocou plusu vedľa neho zadajte nový názov.',
'Zoznam úlohy zoskupuje. Vyberte jeden alebo pomocou plusu vedľa neho pomenujte nový v malom dialógu.',
'help.guide.create-todo.step.4': 'Priorita má štyri tlačidlá: Žiadne, P1, P2 a P3, od červenej po modrú.',
'help.guide.create-todo.step.5': 'Termín splnenia otvorí kalendár a Priradené priradí úlohu konkrétnej osobe.',
'help.guide.create-todo.step.6': 'Kliknite na Vytvoriť úlohu.',
@@ -3215,16 +3215,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Pod Zoznamy je jeden riadok pre každý zoznam. Výberom sa zobrazí daný zoznam vrátane dokončených úloh.',
'help.guide.todo-filters.step.3':
'Zoradiť podľa mení poradie toho, čo je na obrazovke: Priorita dá na začiatok P1, Termín splnenia najbližší termín. Naraz platí iba jedno z nich.',
'Zoradenie v hlavičke zoznamu mení poradie toho, čo je na obrazovke: Priorita dá na začiatok P1, Termín splnenia najbližší termín. Naraz platí iba jedno z nich a druhé kliknutie vráti vaše vlastné poradie.',
'help.guide.todo-filters.step.4': 'Kliknutím na úlohu ju otvoríte v paneli vpravo.',
'help.guide.todo-filters.step.5':
'Zmeňte, čo potrebujete: Popis, Priorita, Zoznam, Termín splnenia alebo Priradené, potom Uložiť zmeny. Vymazať úlohu okamžite odstráni.',
'Zmeňte, čo potrebujete: Popis, Priorita, Zoznam, Termín splnenia alebo Priradené, potom Uložiť zmeny. Začiarkavacie políčko v hlavičke panela označí úlohu ako hotovú a Zmazať ju okamžite odstráni.',
'help.guide.todo-filters.result':
'Zoznam ukazuje iba úlohy, ktoré ste chceli vidieť, a panel vpravo upravuje tú, ktorú ste vybrali.',
'help.guide.todo-filters.tip.1':
'Riadok zoznamu počíta iba to, čo je ešte otvorené, ale po jeho výbere sa zobrazia aj dokončené úlohy. Všetky, Moje úlohy a Po termíne skrývajú hotové; Hotovo nezobrazuje nič iné.',
'help.guide.todo-filters.tip.2':
'Zoradenie podľa priority a podľa termínu splnenia sa navzájom vylučujú a kým je niektoré zapnuté, riadky už nemožno presúvaním usporiadať do vlastného poradia.',
'Priorita a Termín splnenia v zoradení sa navzájom vylučujú a kým je niektoré zapnuté, riadky už nemožno presúvaním usporiadať do vlastného poradia.',
// ── Screen: trip-costs ────────────────────────────────────────────────────────────────
'help.ctx.trip-costs.title': 'Náklady',
+5 -5
View File
@@ -2928,7 +2928,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Använd mall och Spara som mall fyller eller behåller en lista utan att du skriver den, och de två ikonerna bredvid dem exporterar listan, som utskrift, som PDF eller som fil, och importerar en. Den röda knappen bredvid förloppsstapeln säger hur många föremål som är markerade och sopar bort dem.',
'help.ctx.trip-lists.bullet.6':
'Att göra har en egen sidopanel: framstegskortet, filtren Alla, Mina uppgifter, Förfallen och Slutförda, Sortera efter Prioritet eller Förfallodag, och en rad per lista. Ett klick på en uppgift öppnar den i rutan till höger, och Lägg till ny uppgift öppnar formuläret Ny uppgift över mitten av skärmen.',
'Att göra har en egen sidopanel: framstegskortet, filtren Alla, Mina uppgifter, Förfallen och Slutförda, en rad per lista och Lägg till lista under dem. Uppgifterna ligger i ett kort vars huvud namnger filtret och rymmer sorteringen, Prioritet eller Förfallodag. Ett klick på en uppgift öppnar den i rutan till höger, och Lägg till ny uppgift öppnar formuläret Ny uppgift över mitten av skärmen.',
// packing-categories
'help.guide.packing-categories.title': 'Bygg packlistan',
'help.guide.packing-categories.goal':
@@ -3071,7 +3071,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.1': 'Klicka på Lägg till ny uppgift högst upp till höger.',
'help.guide.create-todo.step.2': 'Ge den ett namn i Uppgiftsnamn, och lägg allt värt att minnas under Beskrivning.',
'help.guide.create-todo.step.3':
'Lista grupperar uppgiften. Välj en, eller använd plusset bredvid för att skriva ett nytt namn.',
'Lista grupperar uppgiften. Välj en, eller använd plusset bredvid för att namnge en ny i en liten dialog.',
'help.guide.create-todo.step.4': 'Prioritet är fyra knappar: Ingen, P1, P2 och P3, rött ner till blått.',
'help.guide.create-todo.step.5': 'Förfallodag öppnar en kalender, och Tilldelad till sätter ett namn på uppgiften.',
'help.guide.create-todo.step.6': 'Klicka på Skapa uppgift.',
@@ -3089,16 +3089,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Under Listor sitter en rad per lista. Att välja en visar den listan, slutförda uppgifter inräknade.',
'help.guide.todo-filters.step.3':
'Sortera efter ordnar om det som står på skärmen: Prioritet lägger P1 först, Förfallodag lägger den närmaste deadlinen först. Bara en av de två åt gången.',
'Sorteringen i listans huvud ordnar om det som står på skärmen: Prioritet lägger P1 först, Förfallodag lägger den närmaste deadlinen först. Bara en av de två åt gången, och ett andra klick går tillbaka till din egen ordning.',
'help.guide.todo-filters.step.4': 'Klicka på en uppgift för att öppna den i rutan till höger.',
'help.guide.todo-filters.step.5':
'Ändra det du behöver, Beskrivning, Prioritet, Lista, Förfallodag eller Tilldelad till, sedan Spara ändringar. Radera tar bort uppgiften på en gång.',
'Ändra det du behöver, Beskrivning, Prioritet, Lista, Förfallodag eller Tilldelad till, sedan Spara ändringar. Kryssrutan i rutans huvud bockar av uppgiften, och Radera tar bort den på en gång.',
'help.guide.todo-filters.result':
'Listan visar bara de uppgifter du bad om, och rutan till höger redigerar den du valde.',
'help.guide.todo-filters.tip.1':
'En listrad räknar bara det som fortfarande är öppet, men att välja den visar de slutförda uppgifterna också. Alla, Mina uppgifter och Förfallen döljer det som är klart; Slutförda visar inget annat.',
'help.guide.todo-filters.tip.2':
'Sortera efter Prioritet och Sortera efter Förfallodag utesluter varandra, och medan någon av dem är på går raderna inte längre att dra i en egen ordning.',
'Prioritet och Förfallodag i sorteringen utesluter varandra, och medan någon av dem är på går raderna inte längre att dra i en egen ordning.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Bokningar',
+6 -5
View File
@@ -2557,7 +2557,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'ใช้เทมเพลต และ บันทึกเป็นเทมเพลต ใช้เติมหรือเก็บรายการโดยไม่ต้องพิมพ์เอง และไอคอนสองอันข้างๆ ใช้ส่งออกรายการเป็นงานพิมพ์ PDF หรือไฟล์ และใช้นำเข้ารายการ ปุ่มสีแดงข้างแถบความคืบหน้าแสดงจำนวนรายการที่ติ๊กไว้และลบรายการเหล่านั้นออก',
'help.ctx.trip-lists.bullet.6':
'สิ่งที่ต้องทำ มีแถบด้านข้างของตัวเอง: การ์ดความคืบหน้า ตัวกรอง ทั้งหมด, งานของฉัน, เกินกำหนด และ เสร็จแล้ว, เรียงตาม ลำดับความสำคัญ หรือ วันครบกำหนด และหนึ่งแถวต่อหนึ่งรายการ คลิกงานเพื่อเปิดในแผงทางขวา และ เพิ่มงานใหม่ จะเปิดแบบฟอร์ม งานใหม่ ตรงกลางหน้าจอ',
'สิ่งที่ต้องทำ มีแถบด้านข้างของตัวเอง: การ์ดความคืบหน้า ตัวกรอง ทั้งหมด, งานของฉัน, เกินกำหนด และ เสร็จแล้ว, หนึ่งแถวต่อหนึ่งรายการ และ เพิ่มรายการ อยู่ใต้แถวเหล่านั้น งานต่างๆ อยู่ในการ์ดที่ส่วนหัวบอกชื่อตัวกรองและมีตัวเรียงลำดับ ลำดับความสำคัญ หรือ วันครบกำหนด คลิกงานเพื่อเปิดในแผงทางขวา และ เพิ่มงานใหม่ จะเปิดแบบฟอร์ม งานใหม่ ตรงกลางหน้าจอ',
// packing-categories
'help.guide.packing-categories.title': 'สร้างรายการจัดกระเป๋า',
'help.guide.packing-categories.goal': 'จัดกลุ่มสิ่งที่คุณจะนำไปเป็นหมวดหมู่ เติมสิ่งของ และระบุว่าใครดูแลแต่ละหมวด',
@@ -2677,7 +2677,8 @@ const help: TranslationStrings = {
'help.guide.create-todo.goal': 'จดสิ่งที่ต้องทำ พร้อมรายการ ลำดับความสำคัญ วันที่ และชื่อผู้รับผิดชอบ',
'help.guide.create-todo.step.1': 'คลิก เพิ่มงานใหม่ ที่มุมขวาบน',
'help.guide.create-todo.step.2': 'ตั้งชื่อใน ชื่องาน และใส่สิ่งที่ควรจำไว้ใน คำอธิบาย',
'help.guide.create-todo.step.3': 'รายการ ใช้จัดกลุ่มงาน เลือกหนึ่งรายการ หรือใช้เครื่องหมายบวกข้างๆ เพื่อพิมพ์ชื่อใหม่',
'help.guide.create-todo.step.3':
'รายการ ใช้จัดกลุ่มงาน เลือกหนึ่งรายการ หรือใช้เครื่องหมายบวกข้างๆ เพื่อตั้งชื่อรายการใหม่ในกล่องโต้ตอบเล็กๆ',
'help.guide.create-todo.step.4': 'ลำดับความสำคัญ มีสี่ปุ่ม: ไม่มี, P1, P2 และ P3 ไล่จากสีแดงไปจนถึงสีน้ำเงิน',
'help.guide.create-todo.step.5': 'วันครบกำหนด จะเปิดปฏิทิน และ มอบหมายให้ ใช้ใส่ชื่อผู้รับผิดชอบงาน',
'help.guide.create-todo.step.6': 'คลิก สร้างงาน',
@@ -2693,15 +2694,15 @@ const help: TranslationStrings = {
'งาน ในแถบด้านข้าง: ทั้งหมด คือทุกอย่างที่ยังค้างอยู่ งานของฉัน คือสิ่งที่เป็นหน้าที่ของคุณ เกินกำหนด คือสิ่งที่มีวันที่ผ่านมาแล้ว เสร็จแล้ว คือสิ่งที่เสร็จสิ้น แต่ละอันแสดงจำนวนของตัวเอง คลิก เกินกำหนด',
'help.guide.todo-filters.step.2': 'ใต้ รายการ มีหนึ่งแถวต่อหนึ่งรายการ การเลือกรายการจะแสดงรายการนั้น รวมถึงงานที่เสร็จแล้ว',
'help.guide.todo-filters.step.3':
'เรียงตาม จะจัดลำดับสิ่งที่อยู่บนหน้าจอใหม่: ลำดับความสำคัญ จะเอา P1 ขึ้นก่อน วันครบกำหนด จะเอากำหนดส่งที่ใกล้ที่สุดขึ้นก่อน ใช้ได้ทีละอย่างเท่านั้น',
'ตัวเรียงลำดับที่ส่วนหัวของรายการจะจัดลำดับสิ่งที่อยู่บนหน้าจอใหม่: ลำดับความสำคัญ จะเอา P1 ขึ้นก่อน วันครบกำหนด จะเอากำหนดส่งที่ใกล้ที่สุดขึ้นก่อน ใช้ได้ทีละอย่างเท่านั้น และการคลิกครั้งที่สองจะกลับไปยังลำดับของคุณเอง',
'help.guide.todo-filters.step.4': 'คลิกงานเพื่อเปิดในแผงด้านขวา',
'help.guide.todo-filters.step.5':
'เปลี่ยนสิ่งที่ต้องการ ได้แก่ คำอธิบาย ลำดับความสำคัญ รายการ วันครบกำหนด หรือ มอบหมายให้ แล้วคลิก บันทึกการเปลี่ยนแปลง ส่วน ลบ จะลบงานทันที',
'เปลี่ยนสิ่งที่ต้องการ ได้แก่ คำอธิบาย ลำดับความสำคัญ รายการ วันครบกำหนด หรือ มอบหมายให้ แล้วคลิก บันทึกการเปลี่ยนแปลง ช่องที่ส่วนหัวของแผงใช้ทำเครื่องหมายว่างานเสร็จแล้ว ส่วน ลบ จะลบงานทันที',
'help.guide.todo-filters.result': 'รายการแสดงเฉพาะงานที่คุณต้องการ และแผงด้านขวาใช้แก้ไขงานที่คุณเลือก',
'help.guide.todo-filters.tip.1':
'แถวของรายการนับเฉพาะงานที่ยังค้างอยู่ แต่เมื่อเลือกจะแสดงงานที่เสร็จแล้วด้วย ทั้งหมด งานของฉัน และ เกินกำหนด จะซ่อนงานที่เสร็จแล้ว ส่วน เสร็จแล้ว จะไม่แสดงอย่างอื่นเลย',
'help.guide.todo-filters.tip.2':
'เรียงตาม ลำดับความสำคัญ และเรียงตาม วันครบกำหนด ใช้พร้อมกันไม่ได้ และขณะที่เปิดอย่างใดอย่างหนึ่งอยู่ คุณจะลากแถวเพื่อจัดลำดับเองไม่ได้',
'ลำดับความสำคัญ และ วันครบกำหนด ในตัวเรียงลำดับใช้พร้อมกันไม่ได้ และขณะที่เปิดอย่างใดอย่างหนึ่งอยู่ คุณจะลากแถวเพื่อจัดลำดับเองไม่ได้',
// ── Screen: trip-costs ────────────────────────────────────────────────────────────────
'help.ctx.trip-costs.title': 'ค่าใช้จ่าย',
+5 -5
View File
@@ -2967,7 +2967,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Şablon uygula ve Şablon olarak kaydet bir listeyi yazmadan doldurur ya da saklar, yanlarındaki iki simge ise listeyi çıktı, PDF ya da dosya olarak dışa aktarır ve bir liste içe aktarır. İlerleme çubuğunun yanındaki kırmızı düğme kaç öğenin işaretli olduğunu söyler ve onları temizler.',
'help.ctx.trip-lists.bullet.6':
'Yapılacaklar kendi kenar çubuğuna sahiptir: ilerleme kartı, Tümü, Görevlerim, Gecikmiş ve Tamamlandı filtreleri, Sırala Öncelik ya da Son tarih ve liste başına bir satır. Bir göreve tıklamak onu sağdaki bölmede açar, Yeni görev ekle ise ekranın ortasında Yeni görev formunu açar.',
'Yapılacaklar kendi kenar çubuğuna sahiptir: ilerleme kartı, Tümü, Görevlerim, Gecikmiş ve Tamamlandı filtreleri, liste başına bir satır ve bunların altında Liste ekle. Görevler bir kartın içindedir; kartın başlığında filtrenin adı ve sıralama, yani Öncelik ya da Son tarih, yer alır. Bir göreve tıklamak onu sağdaki bölmede açar, Yeni görev ekle ise ekranın ortasında Yeni görev formunu açar.',
// packing-categories
'help.guide.packing-categories.title': 'Paket listesini kurun',
'help.guide.packing-categories.goal':
@@ -3111,7 +3111,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.2':
'Görev adı alanında ona bir ad verin ve hatırlanmaya değer ne varsa Açıklama altına koyun.',
'help.guide.create-todo.step.3':
'Liste görevi gruplar. Birini seçin ya da yanındaki artıyı kullanarak yeni bir ad yazın.',
'Liste görevi gruplar. Birini seçin ya da yanındaki artıyı kullanarak küçük bir iletişim kutusunda yenisine ad verin.',
'help.guide.create-todo.step.4': 'Öncelik dört düğmedir: Yok, P1, P2 ve P3, kırmızıdan maviye.',
'help.guide.create-todo.step.5': 'Son tarih bir takvim açar, Atanan ise göreve bir ad koyar.',
'help.guide.create-todo.step.6': 'Görev oluştur düğmesine tıklayın.',
@@ -3130,16 +3130,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Listeler altında liste başına bir satır oturur. Birini seçmek o listeyi, bitmiş görevler dahil, gösterir.',
'help.guide.todo-filters.step.3':
'Sırala ekrandakinin sırasını değiştirir: Öncelik P1 olanları öne alır, Son tarih en yakın tarihi öne alır. Aynı anda yalnızca ikisinden biri.',
'Listenin başlığındaki sıralama ekrandakinin sırasını değiştirir: Öncelik P1 olanları öne alır, Son tarih en yakın tarihi öne alır. Aynı anda yalnızca ikisinden biri, ikinci bir tıklama ise sizin kendi sıranıza geri döner.',
'help.guide.todo-filters.step.4': 'Sağdaki bölmede açmak için bir göreve tıklayın.',
'help.guide.todo-filters.step.5':
'Neyi gerekiyorsa değiştirin, Açıklama, Öncelik, Liste, Son tarih ya da Atanan, sonra Değişiklikleri kaydet. Sil görevi hemen alır götürür.',
'Neyi gerekiyorsa değiştirin, Açıklama, Öncelik, Liste, Son tarih ya da Atanan, sonra Değişiklikleri kaydet. Bölmenin başlığındaki kutu görevi tamamlandı olarak işaretler, Sil ise onu hemen alır götürür.',
'help.guide.todo-filters.result':
'Liste yalnızca istediğiniz görevleri gösterir ve sağdaki bölme seçtiğinizi düzenler.',
'help.guide.todo-filters.tip.1':
'Bir liste satırı yalnızca hâlâ açık olanı sayar, ama onu seçmek bitmiş görevleri de gösterir. Tümü, Görevlerim ve Gecikmiş bitmiş olanı gizler; Tamamlandı başka bir şey göstermez.',
'help.guide.todo-filters.tip.2':
'Sırala Öncelik ve Sırala Son tarih birbirini dışlar ve ikisinden biri açıkken satırlar artık kendi istediğiniz bir sıraya sürüklenemez.',
'Sıralamadaki Öncelik ve Son tarih birbirini dışlar ve ikisinden biri açıkken satırlar artık kendi istediğiniz bir sıraya sürüklenemez.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Rezervasyonlar',
+5 -5
View File
@@ -2948,7 +2948,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'Застосувати шаблон і Зберегти як шаблон наповнюють або зберігають список без набирання вручну, а дві іконки поруч експортують список, як роздруківку, PDF або файл, та імпортують список. Червона кнопка поруч зі смугою прогресу називає, скільки речей позначено, і прибирає їх.',
'help.ctx.trip-lists.bullet.6':
'Задачі мають власну бічну панель: картку прогресу, фільтри Усі, Мої задачі, Просрочені та Виконані, Сорзувати за Пріоритет або Строк виконання і по одному рядку на список. Натискання на задачу відкриває її в панелі праворуч, а Нова задача відкриває посеред екрана форму Нова задача.',
'Задачі мають власну бічну панель: картку прогресу, фільтри Усі, Мої задачі, Просрочені та Виконані, по одному рядку на список і під ними Додати список. Задачі лежать у картці, в заголовку якої вказано фільтр і розташовано сортування, Пріоритет або Строк виконання. Натискання на задачу відкриває її в панелі праворуч, а Нова задача відкриває посеред екрана форму Нова задача.',
// packing-categories
'help.guide.packing-categories.title': 'Побудувати список речей',
'help.guide.packing-categories.goal':
@@ -3085,7 +3085,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.1': 'Натисніть Нова задача праворуч угорі.',
'help.guide.create-todo.step.2': 'Назвіть її в Назва завдання і покладіть усе, що варто запам’ятати, під Опис.',
'help.guide.create-todo.step.3':
'Список групує задачу. Виберіть один або скористайтеся плюсом поруч, щоб ввести нову назву.',
'Список групує задачу. Виберіть один або скористайтеся плюсом поруч, щоб назвати новий у невеликому діалозі.',
'help.guide.create-todo.step.4': 'Пріоритет це чотири кнопки: Немає, P1, P2 і P3, від червоного до синього.',
'help.guide.create-todo.step.5': 'Строк виконання відкриває календар, а Назначено ставить на задачу ім’я.',
'help.guide.create-todo.step.6': 'Натисніть Створити задачу.',
@@ -3103,16 +3103,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Під Списки сидить по одному рядку на список. Вибір одного показує цей список, разом із виконаними задачами.',
'help.guide.todo-filters.step.3':
'Сорзувати за змінює порядок того, що на екрані: Пріоритет ставить уперед P1, Строк виконання ставить уперед найближчий строк. Лише одне з двох за раз.',
'Сортування в заголовку списку змінює порядок того, що на екрані: Пріоритет ставить уперед P1, Строк виконання ставить уперед найближчий строк. Лише одне з двох за раз, а повторне натискання повертає ваш власний порядок.',
'help.guide.todo-filters.step.4': 'Натисніть на задачу, щоб відкрити її в панелі праворуч.',
'help.guide.todo-filters.step.5':
'Змініть, що потрібно, Опис, Пріоритет, Список, Строк виконання або Назначено, а потім Зберегти зміни. Видалити прибирає задачу одразу.',
'Змініть, що потрібно, Опис, Пріоритет, Список, Строк виконання або Назначено, а потім Зберегти зміни. Прапорець у заголовку панелі позначає задачу як виконану, а Видалити прибирає її одразу.',
'help.guide.todo-filters.result':
'Список показує лише ті задачі, про які ви попросили, а панель праворуч редагує ту, яку ви вибрали.',
'help.guide.todo-filters.tip.1':
'Рядок списку рахує лише те, що ще відкрите, але його вибір показує й виконані задачі. Усі, Мої задачі та Просрочені приховують те, що зроблено; Виконані не показують нічого іншого.',
'help.guide.todo-filters.tip.2':
'Сорзувати за Пріоритет і Сорзувати за Строк виконання виключають одне одного, і доки одне з них увімкнене, рядки вже не можна перетягувати у власний порядок.',
'Пріоритет і Строк виконання в сортуванні виключають одне одного, і доки одне з них увімкнене, рядки вже не можна перетягувати у власний порядок.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Бронювання',
+5 -5
View File
@@ -2952,7 +2952,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'“Áp dụng mẫu” và “Lưu dưới dạng mẫu” lấp đầy hoặc giữ lại một danh sách mà không phải gõ ra, còn hai biểu tượng bên cạnh xuất danh sách, dưới dạng bản in, PDF hoặc tập tin, và nhập một danh sách. Nút đỏ bên cạnh thanh tiến độ nêu có bao nhiêu mục đã được đánh dấu rồi dọn chúng đi.',
'help.ctx.trip-lists.bullet.6':
'“Việc cần làm” có thanh bên riêng: thẻ tiến độ, các bộ lọc “Tất cả”, “Nhiệm vụ của tôi”, “Quá hạn” và “Xong”, “Sắp xếp theo” “Sự ưu tiên” hoặc “Ngày đến hạn”, và một hàng cho mỗi danh sách. Nhấp vào một nhiệm vụ sẽ mở nó ở khung bên phải, và “Thêm nhiệm vụ mới” mở biểu mẫu “Nhiệm vụ mới” ngay giữa màn hình.',
'“Việc cần làm” có thanh bên riêng: thẻ tiến độ, các bộ lọc “Tất cả”, “Nhiệm vụ của tôi”, “Quá hạn” và “Xong”, một hàng cho mỗi danh sách và “Thêm danh sách” bên dưới. Các nhiệm vụ nằm trong một thẻ có phần đầu ghi tên bộ lọc và chứa cách sắp xếp, “Sự ưu tiên” hoặc “Ngày đến hạn”. Nhấp vào một nhiệm vụ sẽ mở nó ở khung bên phải, và “Thêm nhiệm vụ mới” mở biểu mẫu “Nhiệm vụ mới” ngay giữa màn hình.',
// packing-categories
'help.guide.packing-categories.title': 'Dựng danh sách đóng gói',
'help.guide.packing-categories.goal':
@@ -3095,7 +3095,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.step.1': 'Nhấp “Thêm nhiệm vụ mới” ở trên cùng bên phải.',
'help.guide.create-todo.step.2': 'Đặt tên ở “Tên nhiệm vụ”, và ghi mọi thứ đáng nhớ vào “Miêu tả”.',
'help.guide.create-todo.step.3':
'“Danh sách” gom nhóm nhiệm vụ. Chọn một, hoặc dùng dấu cộng bên cạnh để gõ một tên mới.',
'“Danh sách” gom nhóm nhiệm vụ. Chọn một, hoặc dùng dấu cộng bên cạnh để đặt tên cho một danh sách mới trong một hộp thoại nhỏ.',
'help.guide.create-todo.step.4': '“Sự ưu tiên” là bốn nút: “Không có”, P1, P2 và P3, từ đỏ xuống xanh lam.',
'help.guide.create-todo.step.5': '“Ngày đến hạn” mở một lịch, và “Được giao cho” đặt một cái tên lên nhiệm vụ.',
'help.guide.create-todo.step.6': 'Nhấp “Tạo nhiệm vụ”.',
@@ -3114,16 +3114,16 @@ const help: TranslationStrings = {
'help.guide.todo-filters.step.2':
'Dưới “Danh sách” là một hàng cho mỗi danh sách. Chọn một hàng sẽ hiện danh sách đó, gồm cả các nhiệm vụ đã hoàn thành.',
'help.guide.todo-filters.step.3':
'“Sắp xếp theo” xếp lại những gì đang trên màn hình: “Sự ưu tiên” đưa P1 lên trước, “Ngày đến hạn” đưa hạn gần nhất lên trước. Mỗi lần chỉ một trong hai.',
'Phần sắp xếp ở đầu danh sách xếp lại những gì đang trên màn hình: “Sự ưu tiên” đưa P1 lên trước, “Ngày đến hạn” đưa hạn gần nhất lên trước. Mỗi lần chỉ một trong hai, và nhấp lần thứ hai sẽ trở về thứ tự riêng của bạn.',
'help.guide.todo-filters.step.4': 'Nhấp một nhiệm vụ để mở nó ở khung bên phải.',
'help.guide.todo-filters.step.5':
'Đổi những gì bạn cần, “Miêu tả”, “Sự ưu tiên”, “Danh sách”, “Ngày đến hạn” hoặc “Được giao cho”, rồi “Lưu thay đổi”. “Xóa bỏ” xóa nhiệm vụ đi ngay lập tức.',
'Đổi những gì bạn cần, “Miêu tả”, “Sự ưu tiên”, “Danh sách”, “Ngày đến hạn” hoặc “Được giao cho”, rồi “Lưu thay đổi”. Ô ở đầu khung đánh dấu nhiệm vụ là đã xong, và “Xóa bỏ” xóa nó đi ngay lập tức.',
'help.guide.todo-filters.result':
'Danh sách chỉ hiện những nhiệm vụ bạn đã yêu cầu, và khung bên phải sửa nhiệm vụ bạn đã chọn.',
'help.guide.todo-filters.tip.1':
'Một hàng danh sách chỉ đếm những gì còn dang dở, nhưng chọn nó thì các nhiệm vụ đã hoàn thành cũng hiện ra. “Tất cả”, “Nhiệm vụ của tôi” và “Quá hạn” giấu những gì đã xong; “Xong” thì không hiện gì khác.',
'help.guide.todo-filters.tip.2':
'“Sắp xếp theo” “Sự ưu tiên” và “Sắp xếp theo” “Ngày đến hạn” loại trừ lẫn nhau, và khi một trong hai đang bật thì không còn kéo các hàng vào thứ tự riêng của bạn được nữa.',
'“Sự ưu tiên” và “Ngày đến hạn” trong phần sắp xếp loại trừ lẫn nhau, và khi một trong hai đang bật thì không còn kéo các hàng vào thứ tự riêng của bạn được nữa.',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': 'Đặt chỗ',
+5 -5
View File
@@ -2472,7 +2472,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'「應用模板」和「儲存為範本」不用一條條敲就能填滿或留下一份清單,旁邊的兩個圖示把清單匯出為列印稿、PDF 或檔案,也能匯入一份。進度條旁邊的紅色按鈕寫出有多少物品已勾選,並把它們清走。',
'help.ctx.trip-lists.bullet.6':
'「待辦事項」有自己的側欄:進度卡片,「全部」「我的任務」「已逾期」「已完成」幾個篩選,依「優先順序」或「到期日」的「排序方式」,以及每份清單一列。點選一個任務會在右側面板裡開啟它,「新增任務」則在螢幕中央開啟「新任務」表單。',
'「待辦事項」有自己的側欄:進度卡片,「全部」「我的任務」「已逾期」「已完成」幾個篩選,每份清單一列,下面是「新增清單」。任務放在一張卡片裡,卡片的標題列寫著篩選名稱,並帶有排序:「優先順序」或「到期日」。點選一個任務會在右側面板裡開啟它,「新增任務」則在螢幕中央開啟「新任務」表單。',
// packing-categories
'help.guide.packing-categories.title': '建立行李清單',
'help.guide.packing-categories.goal': '把要帶的東西歸進清單,填上物品,並說明誰來負責每一份清單。',
@@ -2592,7 +2592,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.goal': '把必須發生的事記下來,配上清單、優先順序、日期和負責人。',
'help.guide.create-todo.step.1': '點選右上角的「新增任務」。',
'help.guide.create-todo.step.2': '在「任務名稱」裡給它取名,把值得記住的內容寫在「說明」下面。',
'help.guide.create-todo.step.3': '「清單」把任務歸類。選一個,或者用旁邊的加號敲一個新名字。',
'help.guide.create-todo.step.3': '「清單」把任務歸類。選一個,或者用旁邊的加號在一個小對話框裡替新清單命名。',
'help.guide.create-todo.step.4': '「優先順序」是四個按鈕:「無」、P1、P2 和 P3,從紅到藍。',
'help.guide.create-todo.step.5': '「到期日」開啟一個日曆,「指派給」給任務寫上一個名字。',
'help.guide.create-todo.step.6': '點選「建立任務」。',
@@ -2608,15 +2608,15 @@ const help: TranslationStrings = {
'側欄裡的「任務」:「全部」是所有還沒完成的,「我的任務」是落在你身上的,「已逾期」是日期已經過去的,「已完成」是做完的。每一項都帶著自己的數字;點選「已逾期」。',
'help.guide.todo-filters.step.2': '「清單」下面每份清單一列。選中一列就顯示那份清單,連已完成的任務也在內。',
'help.guide.todo-filters.step.3':
'「排序方式」重新排列螢幕上的內容:「優先順序」把 P1 放最前,「到期日」把最近的期限放最前。兩者一次只能用一個。',
'清單標題列裡的排序會重新排列螢幕上的內容:「優先順序」把 P1 放最前,「到期日」把最近的期限放最前。兩者一次只能用一個,再點一次就回到你自己的順序。',
'help.guide.todo-filters.step.4': '點選一個任務,在右側面板裡開啟它。',
'help.guide.todo-filters.step.5':
'改你需要改的,「說明」「優先順序」「清單」「到期日」或「指派給」,然後「儲存變更」。「刪除」立刻把任務刪掉。',
'改你需要改的,「說明」「優先順序」「清單」「到期日」或「指派給」,然後「儲存變更」。面板標題列裡的核取方塊把任務勾為完成,「刪除」立刻把它刪掉。',
'help.guide.todo-filters.result': '清單只顯示你要的任務,右側面板編輯你選中的那一個。',
'help.guide.todo-filters.tip.1':
'清單那一列只數還沒完成的,但選中它連已完成的任務也會顯示。「全部」「我的任務」和「已逾期」藏起做完的;「已完成」則只顯示做完的。',
'help.guide.todo-filters.tip.2':
'依「優先順序」排序和依「到期日」排序互相排斥,而且只要其中一個開著,列就不能再拖成你自己的順序了。',
'排序裡的「優先順序」和「到期日」互相排斥,而且只要其中一個開著,列就不能再拖成你自己的順序了。',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': '預訂',
+5 -5
View File
@@ -2426,7 +2426,7 @@ const help: TranslationStrings = {
'help.ctx.trip-lists.bullet.5':
'“应用模板”和“保存为模板”不用一条条敲就能填满或留下一份清单,旁边的两个图标把清单导出为打印件、PDF 或文件,也能导入一份。进度条旁边的红色按钮写出有多少物品已勾选,并把它们清走。',
'help.ctx.trip-lists.bullet.6':
'“待办事项”有自己的侧栏:进度卡片,“全部”“我的任务”“已逾期”“已完成”几个筛选,按“优先级”或“截止日期”的“排序方式”,以及每份清单一行。点击一个任务会在右侧面板里打开它,“新建任务”则在屏幕中央打开“新任务”表单。',
'“待办事项”有自己的侧栏:进度卡片,“全部”“我的任务”“已逾期”“已完成”几个筛选,每份清单一行,下面是“添加清单”。任务放在一张卡片里,卡片的标题栏写着筛选名称,并带有排序:“优先级”或“截止日期”。点击一个任务会在右侧面板里打开它,“新建任务”则在屏幕中央打开“新任务”表单。',
// packing-categories
'help.guide.packing-categories.title': '搭建行李清单',
'help.guide.packing-categories.goal': '把要带的东西归进清单,填上物品,并说明谁来负责每一份清单。',
@@ -2546,7 +2546,7 @@ const help: TranslationStrings = {
'help.guide.create-todo.goal': '把必须发生的事记下来,配上清单、优先级、日期和负责人。',
'help.guide.create-todo.step.1': '点击右上角的“新建任务”。',
'help.guide.create-todo.step.2': '在“任务名称”里给它起名,把值得记住的内容写在“描述”下面。',
'help.guide.create-todo.step.3': '“清单”把任务归类。选一个,或者用旁边的加号敲一个新名字。',
'help.guide.create-todo.step.3': '“清单”把任务归类。选一个,或者用旁边的加号在一个小对话框里给新清单起名。',
'help.guide.create-todo.step.4': '“优先级”是四个按钮:“无”、P1、P2 和 P3,从红到蓝。',
'help.guide.create-todo.step.5': '“截止日期”打开一个日历,“分配给”给任务写上一个名字。',
'help.guide.create-todo.step.6': '点击“创建任务”。',
@@ -2562,15 +2562,15 @@ const help: TranslationStrings = {
'侧栏里的“任务”:“全部”是所有还没完成的,“我的任务”是落在你身上的,“已逾期”是日期已经过去的,“已完成”是做完的。每一项都带着自己的数字;点击“已逾期”。',
'help.guide.todo-filters.step.2': '“清单”下面每份清单一行。选中一行就显示那份清单,连已完成的任务也在内。',
'help.guide.todo-filters.step.3':
'“排序方式”重新排列屏幕上的内容:“优先级”把 P1 放最前,“截止日期”把最近的期限放最前。两者一次只能用一个。',
'清单标题栏里的排序会重新排列屏幕上的内容:“优先级”把 P1 放最前,“截止日期”把最近的期限放最前。两者一次只能用一个,再点一次就回到你自己的顺序。',
'help.guide.todo-filters.step.4': '点击一个任务,在右侧面板里打开它。',
'help.guide.todo-filters.step.5':
'改你需要改的,“描述”“优先级”“清单”“截止日期”或“分配给”,然后“保存更改”。“删除”立刻把任务删掉。',
'改你需要改的,“描述”“优先级”“清单”“截止日期”或“分配给”,然后“保存更改”。面板标题栏里的复选框把任务勾为完成,“删除”立刻把它删掉。',
'help.guide.todo-filters.result': '列表只显示你要的任务,右侧面板编辑你选中的那一个。',
'help.guide.todo-filters.tip.1':
'清单那一行只数还没完成的,但选中它连已完成的任务也会显示。“全部”“我的任务”和“已逾期”藏起做完的;“已完成”则只显示做完的。',
'help.guide.todo-filters.tip.2':
'按“优先级”排序和按“截止日期”排序互相排斥,而且只要其中一个开着,行就不能再拖成你自己的顺序了。',
'排序里的“优先级”和“截止日期”互相排斥,而且只要其中一个开着,行就不能再拖成你自己的顺序了。',
// ── Screen: trip-bookings ─────────────────────────────────────────────────────────────
'help.ctx.trip-bookings.title': '预订',
+5 -5
View File
@@ -14,10 +14,10 @@ Open the **Lists** tab inside the trip planner and select **Todo**. The Todo fea
The panel is divided into two columns by default:
- **Left sidebar** — list navigation, smart filters, sort toggle, and a completion progress card.
- **Task list** — the tasks that match the active filter.
- **Left sidebar**: a progress card (done of all, as a count, a percentage and a bar), the smart filters, one row per list, and **Add list**, which asks for the name in a dialog.
- **Task list**: a card whose head names the active filter with its count and holds the sort, **Priority** or **Due date**. Each row is a checkbox, the name, the description, the priority, due date and list as small badges, and the assignee as an avatar with the name as its tooltip.
When you click a task, a **detail pane** opens as a third column on the right side of the task list (on desktop) or slides up as a bottom sheet (on mobile). The new-task form also opens as a modal overlay.
When you click a task, a **detail pane** opens as a third column on the right side of the task list (on desktop) or slides up as a bottom sheet (on mobile). Its head carries a checkbox that ticks the task off, and **Delete** and **Save changes** stay in sight at its foot while the fields scroll. The new-task form opens as a modal overlay with the same fields; the **+** beside the List field names a new list in a dialog.
On small screens the sidebar collapses to a narrow icon rail showing only colored dots and icons with badge counts.
@@ -57,9 +57,9 @@ Tasks with no priority set show no badge.
| **Done** | Checked tasks. |
| Per-list rows | All tasks in that specific list (checked and unchecked). |
## Sort by priority
## Sorting
Toggle the **Priority** button in the sidebar to sort the current task list from P1 → P2 → P3 (tasks with no priority appear last).
The sort sits in the head of the task list. **Priority** orders the tasks from P1 to P2 to P3 (tasks with no priority appear last), **Due date** puts the nearest deadline first. Only one of the two is on at a time; a second click on the active one goes back to your own order, which you can then change by dragging the rows.
## Adding tasks