@atlastodo/shared 0.0.0-stage → 0.1.11-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/activity.d.ts +23 -0
- package/dist/avatarColor.d.ts +15 -0
- package/dist/boardMove.d.ts +14 -0
- package/dist/bugReport.d.ts +47 -0
- package/dist/calendar.d.ts +23 -0
- package/dist/comments.d.ts +14 -0
- package/dist/countdown.d.ts +14 -0
- package/dist/cursor.d.ts +6 -0
- package/dist/dataTransfer.d.ts +19 -0
- package/dist/duplicateProject.d.ts +19 -0
- package/dist/duplicateSection.d.ts +15 -0
- package/dist/filterQuery.d.ts +64 -0
- package/dist/focus.d.ts +17 -0
- package/dist/format.d.ts +30 -0
- package/dist/fuzzy.d.ts +14 -0
- package/dist/grouping.d.ts +24 -0
- package/dist/habitGroups.d.ts +46 -0
- package/dist/habitSchedule.d.ts +4 -0
- package/dist/habits.d.ts +115 -0
- package/dist/hotkeys.d.ts +50 -0
- package/dist/i18n.d.ts +14796 -0
- package/dist/ids.d.ts +7 -0
- package/dist/importTicktick.d.ts +34 -0
- package/dist/importTodoist.d.ts +60 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +21446 -0
- package/dist/members.d.ts +26 -0
- package/dist/planDay.d.ts +31 -0
- package/dist/pomodoro.d.ts +49 -0
- package/dist/priority.d.ts +2 -0
- package/dist/projectMapper.d.ts +15 -0
- package/dist/projectStyle.d.ts +14 -0
- package/dist/projectTree.d.ts +40 -0
- package/dist/quickAddLexicons.d.ts +64 -0
- package/dist/quickAddLexiconsWire.d.ts +11 -0
- package/dist/quickAddParse.d.ts +44 -0
- package/dist/quickSchedule.d.ts +9 -0
- package/dist/rank.d.ts +9 -0
- package/dist/recurrence.d.ts +60 -0
- package/dist/regions.d.ts +6 -0
- package/dist/reminderSchedule.d.ts +42 -0
- package/dist/reminders.d.ts +28 -0
- package/dist/reorder.d.ts +21 -0
- package/dist/sectionMapper.d.ts +8 -0
- package/dist/sectionOps.d.ts +5 -0
- package/dist/serverUrl.d.ts +12 -0
- package/dist/smartLists.d.ts +11 -0
- package/dist/stats.d.ts +28 -0
- package/dist/swipe.d.ts +29 -0
- package/dist/taskClipboard.d.ts +19 -0
- package/dist/taskListTree.d.ts +33 -0
- package/dist/taskMapper.d.ts +3 -0
- package/dist/taskOps.d.ts +49 -0
- package/dist/taskSearch.d.ts +11 -0
- package/dist/taskTree.d.ts +53 -0
- package/dist/theme.d.ts +14 -0
- package/dist/toast.d.ts +26 -0
- package/dist/trash.d.ts +24 -0
- package/dist/views.d.ts +8 -0
- package/dist/zonedTime.d.ts +30 -0
- package/package.json +55 -4
- package/test-vectors/filter_vectors.json +1613 -0
- package/test-vectors/quick_add_vectors.json +1656 -0
- package/test-vectors/recurrence_vectors.json +779 -0
- package/test-vectors/task_rules_vectors.json +729 -0
- package/README.md +0 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MemberRole } from "@atlastodo/client-core";
|
|
2
|
+
/**
|
|
3
|
+
* A shared-project member as stored locally. The server authors `project_member` entities into each
|
|
4
|
+
* member's sync partition, so the collaborator list and assignee picker read from the local store,
|
|
5
|
+
* offline included. The client never writes them; membership changes go through the REST endpoints.
|
|
6
|
+
*/
|
|
7
|
+
export interface Member {
|
|
8
|
+
id: string;
|
|
9
|
+
project_id: string;
|
|
10
|
+
user_id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
display_name: string;
|
|
13
|
+
role: MemberRole;
|
|
14
|
+
state: "pending" | "active";
|
|
15
|
+
deletion_scheduled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function toMember(id: string, fields: Record<string, unknown>): Member;
|
|
18
|
+
/** Whether `projectId` is shared, has active owners, and all of them are scheduled for deletion. */
|
|
19
|
+
export declare function isOwnerDeletionScheduled(members: Pick<Member, "project_id" | "role" | "state" | "deletion_scheduled">[], projectId: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `userId` may act as owner of `projectId`, the gate for destructive actions (mirrors the
|
|
22
|
+
* server's owner-only rule). A project with no member rows is private to its creator, who is the
|
|
23
|
+
* implicit owner. Once shared, only an active `role: "owner"` qualifies; others can only leave.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isProjectOwner(members: Pick<Member, "project_id" | "user_id" | "role" | "state">[], projectId: string, userId: string | undefined): boolean;
|
|
26
|
+
export declare function initialsOf(member: Pick<Member, "display_name" | "email">): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Task } from "@atlastodo/client-core";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers for the Today screen's plan-my-day review. The only persisted outcome is due-date
|
|
4
|
+
* changes (`planDayWrites` reduces decisions to plain `{ id, dueAt }` writes).
|
|
5
|
+
*/
|
|
6
|
+
export declare const PLAN_DAY_HORIZON_DAYS = 7;
|
|
7
|
+
export type PlanDayBucket = "overdue" | "today" | "upcoming";
|
|
8
|
+
export interface PlanDayItem {
|
|
9
|
+
task: Task;
|
|
10
|
+
bucket: PlanDayBucket;
|
|
11
|
+
}
|
|
12
|
+
export declare function planDayItems(tasks: Task[], now: number, timeZone?: string): PlanDayItem[];
|
|
13
|
+
export declare function planDayUpcoming(tasks: Task[], now: number, timeZone?: string, days?: number): Task[];
|
|
14
|
+
export interface PlanDayOption {
|
|
15
|
+
offset: number;
|
|
16
|
+
dueAt: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function planDayPostponeOptions(now: number, timeZone?: string, days?: number): PlanDayOption[];
|
|
19
|
+
export type PlanDayDecision = {
|
|
20
|
+
kind: "keep";
|
|
21
|
+
} | {
|
|
22
|
+
kind: "postpone";
|
|
23
|
+
dueAt: number;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "later";
|
|
26
|
+
};
|
|
27
|
+
export interface PlanDayWrite {
|
|
28
|
+
id: string;
|
|
29
|
+
dueAt: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function planDayWrites(items: PlanDayItem[], decisions: Record<string, PlanDayDecision | undefined>, todayEnd: number): PlanDayWrite[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pomodoro state machine: pure and time-free. The running clock lives in the FocusProvider; this
|
|
3
|
+
* decides which phase comes next and how long each lasts.
|
|
4
|
+
*/
|
|
5
|
+
export type Phase = "work" | "short_break" | "long_break";
|
|
6
|
+
export interface PomodoroConfig {
|
|
7
|
+
workMin: number;
|
|
8
|
+
shortBreakMin: number;
|
|
9
|
+
longBreakMin: number;
|
|
10
|
+
longBreakEvery: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_POMODORO: PomodoroConfig;
|
|
13
|
+
export type FocusCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
14
|
+
export declare const FOCUS_CORNERS: FocusCorner[];
|
|
15
|
+
export declare const DEFAULT_FOCUS_CORNER: FocusCorner;
|
|
16
|
+
export declare function isFocusCorner(value: unknown): value is FocusCorner;
|
|
17
|
+
export declare function phaseDurationMs(phase: Phase, config: PomodoroConfig): number;
|
|
18
|
+
export declare function formatClock(ms: number): string;
|
|
19
|
+
export declare function nextPhase(phase: Phase, completedWork: number, config: PomodoroConfig): Phase;
|
|
20
|
+
export interface PomodoroState {
|
|
21
|
+
phase: Phase;
|
|
22
|
+
completedWork: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function advance(state: PomodoroState, config: PomodoroConfig): PomodoroState;
|
|
25
|
+
export interface RunSnapshot {
|
|
26
|
+
taskId: string | null;
|
|
27
|
+
phase: Phase;
|
|
28
|
+
completedWork: number;
|
|
29
|
+
remainingMs: number;
|
|
30
|
+
running: boolean;
|
|
31
|
+
awaitingStart: boolean;
|
|
32
|
+
workStartedAt: number;
|
|
33
|
+
workElapsedMs: number;
|
|
34
|
+
}
|
|
35
|
+
export interface FocusSessionLog {
|
|
36
|
+
taskId: string | null;
|
|
37
|
+
startedAt: number;
|
|
38
|
+
endedAt: number;
|
|
39
|
+
durationMs: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Recompute a persisted run at a later `now`. A running one completes at most one phase: one that
|
|
43
|
+
* ended while the app was closed parks on the next phase with `awaitingStart`, rather than
|
|
44
|
+
* replaying a break nobody was present for.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rehydrateRun(saved: RunSnapshot, savedAt: number, now: number, config: PomodoroConfig): {
|
|
47
|
+
run: RunSnapshot;
|
|
48
|
+
sessions: FocusSessionLog[];
|
|
49
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Project, ProjectKind, ProjectView } from "@atlastodo/client-core";
|
|
2
|
+
export declare function toProject(id: string, fields: Record<string, unknown>): Project;
|
|
3
|
+
/**
|
|
4
|
+
* The fields written when creating a project or folder. `parent_id` is always written, even null,
|
|
5
|
+
* so a later move is a plain update.
|
|
6
|
+
*/
|
|
7
|
+
export declare function projectCreateFields(input: {
|
|
8
|
+
name: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
sort_order?: number;
|
|
12
|
+
kind?: ProjectKind;
|
|
13
|
+
parent_id?: string | null;
|
|
14
|
+
default_view?: ProjectView;
|
|
15
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project icon and colour: the portable half. Each app maps names to its own icon components;
|
|
3
|
+
* keep both keyed by {@link PROJECT_ICON_NAMES}.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_PROJECT_ICON = "hash";
|
|
6
|
+
export declare const DEFAULT_FILTER_ICON = "filter";
|
|
7
|
+
export declare const DEFAULT_FOLDER_ICON = "folder";
|
|
8
|
+
export declare const PROJECT_ICON_NAMES: string[];
|
|
9
|
+
export declare const PROJECT_COLORS: string[];
|
|
10
|
+
export declare function defaultColorForIndex(n: number): string;
|
|
11
|
+
export declare function resolveProjectColor(project: {
|
|
12
|
+
id: string;
|
|
13
|
+
color: string;
|
|
14
|
+
}): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Project } from "@atlastodo/client-core";
|
|
2
|
+
/**
|
|
3
|
+
* The project forest: a flat `Project[]` as a nested render list. A folder is a project with
|
|
4
|
+
* `kind: "folder"` that holds no tasks and groups others via `parent_id`.
|
|
5
|
+
*
|
|
6
|
+
* Exports are named differently from `taskTree`'s because `index.ts` star-exports both (TS2308).
|
|
7
|
+
*
|
|
8
|
+
* Orphans are roots: a co-member of a shared project can receive a `parent_id` naming a folder
|
|
9
|
+
* never shared with them and would otherwise lose the project.
|
|
10
|
+
*
|
|
11
|
+
* Cycles are guarded here only (the server does not validate parents on the sync path), so
|
|
12
|
+
* {@link wouldCycleProject} is the enforcement and every walk carries a `seen` guard.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProjectTreeNode {
|
|
15
|
+
project: Project;
|
|
16
|
+
depth: number;
|
|
17
|
+
children: ProjectTreeNode[];
|
|
18
|
+
}
|
|
19
|
+
export interface FlatProjectRow {
|
|
20
|
+
project: Project;
|
|
21
|
+
depth: number;
|
|
22
|
+
hasChildren: boolean;
|
|
23
|
+
childCount: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildProjectTree(projects: Project[]): ProjectTreeNode[];
|
|
26
|
+
export declare function flattenProjectTree(projects: Project[], collapsed?: ReadonlySet<string>): FlatProjectRow[];
|
|
27
|
+
export declare function projectAncestors(projects: Project[], id: string): Project[];
|
|
28
|
+
export declare function projectDescendantIds(projects: Project[], id: string): Set<string>;
|
|
29
|
+
export declare function wouldCycleProject(projects: Project[], movingId: string, candidateParentId: string | null): boolean;
|
|
30
|
+
export declare function moveProjectTarget(projects: Project[], id: string, newParentId: string | null): {
|
|
31
|
+
parent_id: string | null;
|
|
32
|
+
sort_order: number;
|
|
33
|
+
} | null;
|
|
34
|
+
export declare function hiddenProjectIds(items: ReadonlyArray<{
|
|
35
|
+
id: string;
|
|
36
|
+
parent_id: string | null;
|
|
37
|
+
archived_at: number | null;
|
|
38
|
+
deleted_at: number | null;
|
|
39
|
+
}>): Set<string>;
|
|
40
|
+
export declare function pinnedProjectTree(projects: Project[], isPinned: (projectId: string) => boolean, collapsed?: ReadonlySet<string>): FlatProjectRow[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Freq } from "./recurrence.js";
|
|
2
|
+
/**
|
|
3
|
+
* Quick-add vocabulary, one lexicon per UI language. `quickAddParse` matches the UI language's
|
|
4
|
+
* lexicon first and English after it, so English phrases work in every language. Every phrase is
|
|
5
|
+
* lowercase and space-separated, and is matched accent-insensitively ({@link fold}), so "manana",
|
|
6
|
+
* "lunedi" and "uebermorgen" work as well as the proper spelling.
|
|
7
|
+
*/
|
|
8
|
+
export interface QuickAddLexicon {
|
|
9
|
+
/** Month before day ("jun 3") as well as day before month ("3 jun"). */
|
|
10
|
+
monthFirst?: boolean;
|
|
11
|
+
/** Words for a count of one: "in a week", "om en uge". */
|
|
12
|
+
one: string[];
|
|
13
|
+
/** A unit straight after an `inPrefixes` word means one of it: Polish "za tydzień". */
|
|
14
|
+
inBareUnit?: boolean;
|
|
15
|
+
/** Time prefixes that may be glued to the hour: Danish "kl17", "kl.9:30". */
|
|
16
|
+
gluedTimePrefixes?: string[];
|
|
17
|
+
/** Phrases that name a day relative to today: "morgen" → 1. */
|
|
18
|
+
relativeDays: Record<string, number>;
|
|
19
|
+
/** Weekday names and their inflected/plural forms, Sunday = 0. */
|
|
20
|
+
weekdays: Record<string, number>;
|
|
21
|
+
/** Month names (and the genitive forms dates use), January = 0. */
|
|
22
|
+
months: Record<string, number>;
|
|
23
|
+
/** Day/week/month/year in every form a count can take. */
|
|
24
|
+
units: Record<string, Freq>;
|
|
25
|
+
/** Before a count and unit: "in 3 Tagen" → "in". */
|
|
26
|
+
inPrefixes: string[];
|
|
27
|
+
/** Before a weekday or unit: "nächsten Montag". */
|
|
28
|
+
nextPrefixes: string[];
|
|
29
|
+
/** After a weekday or unit: "lundi prochain". */
|
|
30
|
+
nextSuffixes: string[];
|
|
31
|
+
/** Before a weekday: "diesen Freitag". */
|
|
32
|
+
thisPrefixes: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Words consumed in front of a date only when the date follows: "am Montag", "el lunes",
|
|
35
|
+
* "w środę", "la semaine prochaine". Never matched on their own.
|
|
36
|
+
*/
|
|
37
|
+
leadIns: string[];
|
|
38
|
+
/** Between a day number and its month: "3 de marzo". */
|
|
39
|
+
monthConnectors: string[];
|
|
40
|
+
/** Before a weekday, a unit, or a count and unit: "jede Woche", "alle 2 Wochen". */
|
|
41
|
+
every: string[];
|
|
42
|
+
/** One word or phrase that is a whole rule: "täglich" → daily. */
|
|
43
|
+
single: Record<string, Freq>;
|
|
44
|
+
/** After a rule: schedule from the completion date, like `every!`. */
|
|
45
|
+
fromCompletion: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Before a time. `true` accepts a bare hour ("um 17"); `false` needs minutes ("o 17:30"), for a
|
|
48
|
+
* prefix that is also an everyday word.
|
|
49
|
+
*/
|
|
50
|
+
timePrefixes: Record<string, boolean>;
|
|
51
|
+
/** A word after the hour: "17 Uhr". */
|
|
52
|
+
timeSuffixes: string[];
|
|
53
|
+
/** Letter glued to the hour: "17h30" (French), "17u" (Dutch). */
|
|
54
|
+
hourLetter?: string;
|
|
55
|
+
noon: string[];
|
|
56
|
+
midnight: string[];
|
|
57
|
+
}
|
|
58
|
+
/** Lowercase, strip accents and unify apostrophes, for accent-insensitive matching. */
|
|
59
|
+
export declare function fold(s: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* The (folded) lexicons to match for a UI language such as "de" or "pt-BR", in order: its own, then
|
|
62
|
+
* English. English alone for English, no language, or one without a lexicon.
|
|
63
|
+
*/
|
|
64
|
+
export declare function quickAddLexicons(language: string | undefined): QuickAddLexicon[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quick-add lexicons as plain JSON, for the Rust client (`crates/atlas-client`), which reads
|
|
3
|
+
* them from `crates/atlas-client/src/rules/quick_add_lexicons.json`. Phrase maps become lists of
|
|
4
|
+
* `[phrase, value]` pairs so their order (which breaks ties between equally long matches) survives.
|
|
5
|
+
* `quickAddLexiconsWire.test.ts` fails when that file is stale; regenerate it with
|
|
6
|
+
* `bun packages/shared/scripts/export-quick-add-lexicons.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const QUICK_ADD_LANGUAGES: readonly ["en", "da", "de", "es", "fr", "it", "nl", "pl", "pt"];
|
|
9
|
+
type Wire = Record<string, unknown>;
|
|
10
|
+
export declare function quickAddLexiconsWire(): Record<string, Wire>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CreateTaskInput } from "@atlastodo/client-core";
|
|
2
|
+
import { DEFAULT_DUE_HOUR, DEFAULT_DUE_MINUTE } from "./zonedTime.js";
|
|
3
|
+
/**
|
|
4
|
+
* Natural-language quick-add parsing ("Buy milk tomorrow 5pm #groceries p1") into a
|
|
5
|
+
* {@link CreateTaskInput} plus preview chips. `every!` or a trailing `from completion` schedules
|
|
6
|
+
* from the completion date. Never throws: unrecognized input stays in the title.
|
|
7
|
+
*/
|
|
8
|
+
export { DEFAULT_DUE_HOUR, DEFAULT_DUE_MINUTE };
|
|
9
|
+
export type ChipKind = "due" | "project" | "label" | "priority" | "recurrence";
|
|
10
|
+
export interface ParsedChip {
|
|
11
|
+
kind: ChipKind;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TokenSpan {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
text: string;
|
|
18
|
+
value: string;
|
|
19
|
+
}
|
|
20
|
+
export interface QuickAddContext {
|
|
21
|
+
projectIdByName?: (name: string) => string | null;
|
|
22
|
+
isKnownLabel?: (name: string) => boolean;
|
|
23
|
+
timeZone?: string;
|
|
24
|
+
language?: string;
|
|
25
|
+
ignoreDates?: string[];
|
|
26
|
+
ignoreProjects?: string[];
|
|
27
|
+
ignoreLabels?: string[];
|
|
28
|
+
disableDates?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface DateMatchSpan {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
export interface QuickAddResult {
|
|
36
|
+
input: CreateTaskInput;
|
|
37
|
+
title: string;
|
|
38
|
+
chips: ParsedChip[];
|
|
39
|
+
dateMatch?: DateMatchSpan;
|
|
40
|
+
projectMatch?: TokenSpan;
|
|
41
|
+
labelMatches?: TokenSpan[];
|
|
42
|
+
labels: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare function parseQuickAdd(text: string, now: number, ctx?: QuickAddContext): QuickAddResult;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick reschedule targets for the swipe-left day picker and compose bar: `now` injected, optional
|
|
3
|
+
* IANA `timeZone`. Each lands on 23:59 of its day, so the task is all-day.
|
|
4
|
+
*/
|
|
5
|
+
export interface QuickScheduleOption {
|
|
6
|
+
key: "today" | "tomorrow" | "weekend" | "nextWeek";
|
|
7
|
+
dueAt: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function quickScheduleOptions(now: number, timeZone?: string): QuickScheduleOption[];
|
package/dist/rank.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fractional ranking for `sort_order`. Ordering uses a `f64` `sort_order` so an item can be re-ranked between two neighbours
|
|
3
|
+
* without renumbering the whole list: the new rank is the midpoint of its neighbours' ranks.
|
|
4
|
+
*
|
|
5
|
+
* `before`/`after` are the `sort_order` values of the items that will sit immediately before and
|
|
6
|
+
* after the moved item at its destination; pass `null` when there is no such neighbour (the moved
|
|
7
|
+
* item lands at the start/end, or the list is empty).
|
|
8
|
+
*/
|
|
9
|
+
export declare function rankBetween(before: number | null, after: number | null): number;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task recurrence (asserted against `test-vectors/recurrence_vectors.json`).
|
|
3
|
+
*
|
|
4
|
+
* A compact RRULE subset, e.g. `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE`, plus non-standard
|
|
5
|
+
* `MODE=COMPLETION` (next occurrence based on the completion time).
|
|
6
|
+
*
|
|
7
|
+
* A monthly rule keeps its day in `BYMONTHDAY` ({@link anchorMonthDay}): stepping from the previous
|
|
8
|
+
* occurrence would turn "the 31st" into "the 28th" for good after one February. A yearly rule due
|
|
9
|
+
* on a leap day keeps it the same way (`BYMONTHDAY=29`, the only day a yearly rule takes), so it
|
|
10
|
+
* returns to Feb 29 in leap years.
|
|
11
|
+
*
|
|
12
|
+
* The engine ({@link nextOccurrenceCivil}) steps wall-clock dates and keeps the time of day, so
|
|
13
|
+
* "every day at 23:59" does not drift across DST. Month and leap-day overflows clamp.
|
|
14
|
+
*/
|
|
15
|
+
export type Freq = "daily" | "weekly" | "monthly" | "yearly";
|
|
16
|
+
export type RecurMode = "on_schedule" | "after_completion";
|
|
17
|
+
export interface Rule {
|
|
18
|
+
freq: Freq;
|
|
19
|
+
interval: number;
|
|
20
|
+
byday: number[];
|
|
21
|
+
bymonthday: number | null;
|
|
22
|
+
mode: RecurMode;
|
|
23
|
+
at?: {
|
|
24
|
+
hour: number;
|
|
25
|
+
minute: number;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
export declare const MAX_INTERVAL = 1000;
|
|
29
|
+
export declare function parseRule(input: string): Rule | null;
|
|
30
|
+
export declare function ruleToString(rule: Rule): string;
|
|
31
|
+
export declare function anchorMonthDay(rule: string, dueMs: number, timeZone?: string): string;
|
|
32
|
+
export declare function pinMonthDay(rule: string, dueMs: number, timeZone?: string): string;
|
|
33
|
+
export type Translate = (key: string, params: Record<string, string | number>) => string;
|
|
34
|
+
export declare function formatRule(input: string, t?: Translate): string | null;
|
|
35
|
+
export interface CivilDateTime {
|
|
36
|
+
year: number;
|
|
37
|
+
month: number;
|
|
38
|
+
day: number;
|
|
39
|
+
hour: number;
|
|
40
|
+
minute: number;
|
|
41
|
+
second: number;
|
|
42
|
+
millisecond: number;
|
|
43
|
+
}
|
|
44
|
+
export interface CivilDate {
|
|
45
|
+
year: number;
|
|
46
|
+
month: number;
|
|
47
|
+
day: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function nextOccurrenceCivil(rule: string, anchor: CivilDateTime, from: CivilDate): CivilDateTime | null;
|
|
50
|
+
/**
|
|
51
|
+
* The occurrence after `dueMs` and the rule to store with it (also given the leap-day and month-day
|
|
52
|
+
* anchors of {@link pinMonthDay}). When the next occurrence falls in a DST gap it shows a later
|
|
53
|
+
* time than the series' own, so the rule keeps the series' time (`BYHOUR`/`BYMINUTE`) to step from;
|
|
54
|
+
* the first occurrence that shows it again drops it. `null` when there is no next occurrence.
|
|
55
|
+
*/
|
|
56
|
+
export declare function advanceSeries(rule: string, dueMs: number, fromMs: number, timeZone?: string): {
|
|
57
|
+
next: number;
|
|
58
|
+
rule: string;
|
|
59
|
+
} | null;
|
|
60
|
+
export declare function nextOccurrence(rule: string, anchorMs: number, fromMs: number, timeZone?: string): number | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Task } from "@atlastodo/client-core";
|
|
2
|
+
import { type Reminder } from "./reminders.js";
|
|
3
|
+
/**
|
|
4
|
+
* OS-scheduled reminder planning: decides which local notifications to book and diffs that against
|
|
5
|
+
* what is booked, so reconciling is idempotent. Keys off {@link reminderFireAt}, so it is
|
|
6
|
+
* DST-correct. Platform differences live in the injected {@link ScheduleIO}.
|
|
7
|
+
*/
|
|
8
|
+
export interface ScheduledReminder {
|
|
9
|
+
reminderId: string;
|
|
10
|
+
fireAt: number;
|
|
11
|
+
title: string;
|
|
12
|
+
body?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ScheduleDiff {
|
|
15
|
+
toSchedule: ScheduledReminder[];
|
|
16
|
+
toCancel: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ScheduleIO {
|
|
19
|
+
schedule: (title: string, body: string, at: number, id: string) => void;
|
|
20
|
+
cancel: (id: string) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface ReminderTitleOptions {
|
|
23
|
+
lockedTitle?: string;
|
|
24
|
+
fallbackTitle?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ReminderPlanOptions extends ReminderTitleOptions {
|
|
27
|
+
timeZone?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function reminderTitle(task: Task | undefined, options?: ReminderTitleOptions): string;
|
|
30
|
+
export declare function reminderBody(task: Task | undefined): string;
|
|
31
|
+
export declare function futureReminders(reminders: Reminder[], tasks: Task[], now: number, options?: ReminderPlanOptions): ScheduledReminder[];
|
|
32
|
+
export interface ApplyOptions {
|
|
33
|
+
rebook?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function diffSchedule(desired: ScheduledReminder[], scheduled: Map<string, number>, { rebook }?: ApplyOptions): ScheduleDiff;
|
|
36
|
+
export declare function applySchedule(desired: ScheduledReminder[], scheduled: Map<string, number>, body: string, io: ScheduleIO, options?: ApplyOptions): void;
|
|
37
|
+
export declare function adoptBooked(booked: readonly string[], desired: ScheduledReminder[], scheduled: Map<string, number>): void;
|
|
38
|
+
export declare function reconcileSchedule(reminders: Reminder[], tasks: Task[], now: number, scheduled: Map<string, number>): ScheduleDiff;
|
|
39
|
+
export interface ReconcileOptions extends ReminderPlanOptions, ApplyOptions {
|
|
40
|
+
booked?: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
export declare function reconcileNotifications(reminders: Reminder[], tasks: Task[], now: number, scheduled: Map<string, number>, body: string, io: ScheduleIO, options?: ReconcileOptions): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Task } from "@atlastodo/client-core";
|
|
2
|
+
/**
|
|
3
|
+
* Task reminders: pure scheduling logic. A reminder fires at an absolute time (`at`) or
|
|
4
|
+
* `offset_min_before_due` minutes before the due date. `fired_at` is synced and covers the fire
|
|
5
|
+
* instant it went out for, so a later-moved instant makes it unfired again ({@link isReminderFired}).
|
|
6
|
+
*
|
|
7
|
+
* All-day dues are 23:59 in the task's calendar, so day-based offsets resolve in `timeZone`, not
|
|
8
|
+
* the device-local reading, which disagrees once the device zone differs.
|
|
9
|
+
*/
|
|
10
|
+
export interface Reminder {
|
|
11
|
+
id: string;
|
|
12
|
+
task_id: string;
|
|
13
|
+
at: number | null;
|
|
14
|
+
offset_min_before_due: number | null;
|
|
15
|
+
fired_at: number | null;
|
|
16
|
+
created_at: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function toReminder(id: string, fields: Record<string, unknown>): Reminder;
|
|
19
|
+
export declare function reminderFireAt(reminder: Reminder, task: Task | undefined, timeZone?: string): number | null;
|
|
20
|
+
export declare const MORNING_OF_OFFSET_MIN = 0;
|
|
21
|
+
export declare function morningReminderOffset(dueAt: number | null | undefined, now: number, timeZone?: string): number | null;
|
|
22
|
+
export declare function isReminderFired(reminder: Reminder, fireAt: number): boolean;
|
|
23
|
+
export declare function dueReminders(reminders: Reminder[], tasks: Task[], now: number, timeZone?: string): Reminder[];
|
|
24
|
+
export declare const REMINDER_STALE_AFTER_MS: number;
|
|
25
|
+
export declare function splitDueReminders(reminders: Reminder[], tasks: Task[], now: number, timeZone?: string, staleAfterMs?: number): {
|
|
26
|
+
fire: Reminder[];
|
|
27
|
+
stale: Reminder[];
|
|
28
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RankWrite {
|
|
2
|
+
id: string;
|
|
3
|
+
sort_order: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The writes that place `movedId` at `toIndex` among `siblings`. Tasks without a rank all default
|
|
7
|
+
* to `0` and `rankBetween` returns the tie value between them, so a move would snap back: a single
|
|
8
|
+
* fractional write is kept only when strictly between the neighbours, else siblings are renumbered.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rankMoveWrites(siblings: readonly {
|
|
11
|
+
id: string;
|
|
12
|
+
sort_order: number;
|
|
13
|
+
}[], movedId: string, toIndex: number): RankWrite[];
|
|
14
|
+
export declare function reorderRank<T extends {
|
|
15
|
+
id: string;
|
|
16
|
+
sort_order: number;
|
|
17
|
+
}>(items: T[], from: number, to: number): {
|
|
18
|
+
id: string;
|
|
19
|
+
sort_order: number;
|
|
20
|
+
writes: RankWrite[];
|
|
21
|
+
} | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Section } from "@atlastodo/client-core";
|
|
2
|
+
export declare function toSection(id: string, fields: Record<string, unknown>): Section;
|
|
3
|
+
/** The fields to write when creating a section locally. */
|
|
4
|
+
export declare function sectionCreateFields(input: {
|
|
5
|
+
project_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
sort_order?: number;
|
|
8
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LocalStore, Section, Task } from "@atlastodo/client-core";
|
|
2
|
+
export declare function allSections(store: LocalStore): Section[];
|
|
3
|
+
export declare function visibleSections(store: LocalStore, projectId: string): Section[];
|
|
4
|
+
export declare function moveSectionToProject(store: LocalStore, sectionId: string, targetProjectId: string, now?: number, tasks?: Task[]): void;
|
|
5
|
+
export declare function setSectionArchived(store: LocalStore, sectionId: string, archived: boolean, now?: number): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-URL resolution: pure, so both frontends share the rule. Reading and writing the override
|
|
3
|
+
* is the platform's job. Atlas Todo is self-hosted, so a stored override beats the build-time
|
|
4
|
+
* default.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The effective server URL. A blank override means "not set" (clearing the settings field) and
|
|
8
|
+
* falls back to `fallback`. The trailing slash is removed because callers build
|
|
9
|
+
* `${baseUrl}${path}`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveServerUrl(override: string | null | undefined, fallback: string): string;
|
|
12
|
+
export declare function stripTrailingSlash(url: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Task } from "@atlastodo/client-core";
|
|
2
|
+
export declare function isOverdue(task: Task, now: number, _timeZone?: string): boolean;
|
|
3
|
+
export declare function inboxTasks(tasks: Task[]): Task[];
|
|
4
|
+
export declare function todayTasks(tasks: Task[], now: number, timeZone?: string): Task[];
|
|
5
|
+
export declare function partitionToday(tasks: Task[], now: number, timeZone?: string): {
|
|
6
|
+
overdue: Task[];
|
|
7
|
+
today: Task[];
|
|
8
|
+
};
|
|
9
|
+
export declare function assignedToMe(tasks: Task[], userId: string): Task[];
|
|
10
|
+
export declare function openTasks(tasks: Task[]): Task[];
|
|
11
|
+
export declare function upcomingTasks(tasks: Task[], now: number, timeZone?: string): Task[];
|
package/dist/stats.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Productivity aggregates over completed tasks: pure, ranges passed in. Days are keyed by calendar
|
|
3
|
+
* date in `timeZone` and iteration walks whole date keys (DST-safe), reusing the habit date
|
|
4
|
+
* helpers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Task } from "@atlastodo/client-core";
|
|
7
|
+
export interface DayCount {
|
|
8
|
+
date: string;
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
export interface WeekCount {
|
|
12
|
+
weekStart: string;
|
|
13
|
+
count: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ProjectCount {
|
|
16
|
+
projectId: string | null;
|
|
17
|
+
count: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function completionsByDay(tasks: Task[], fromMs: number, toMs: number, timeZone?: string): DayCount[];
|
|
20
|
+
export declare function completionsByWeek(tasks: Task[], fromMs: number, toMs: number, weekStartsOn?: number, timeZone?: string): WeekCount[];
|
|
21
|
+
export declare function completionsByProject(tasks: Task[], fromMs: number, toMs: number): ProjectCount[];
|
|
22
|
+
export declare function totalCompleted(tasks: Task[], fromMs: number, toMs: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* "Karma" streak: consecutive days ending today with a completed task. Today gets grace (no
|
|
25
|
+
* completions yet does not break it); an earlier empty day does.
|
|
26
|
+
*/
|
|
27
|
+
export declare function completedStreak(tasks: Task[], todayMs: number, timeZone?: string): number;
|
|
28
|
+
export declare function completedHistory(tasks: Task[], query: string): Task[];
|
package/dist/swipe.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Horizontal swipe gestures on task rows. {@link classifySwipe} counts a drag only when it travels
|
|
3
|
+
* far enough and is clearly more horizontal than vertical, so it never hijacks a scroll.
|
|
4
|
+
* {@link resolveTaskSwipe} decides what the configured action means for one row.
|
|
5
|
+
*/
|
|
6
|
+
export interface Point {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
export type SwipeDirection = "right" | "left" | "none";
|
|
11
|
+
export type TaskSwipeAction = "complete" | "schedule" | "indent" | "delete" | "none";
|
|
12
|
+
export declare const TASK_SWIPE_ACTIONS: TaskSwipeAction[];
|
|
13
|
+
export declare function isTaskSwipeAction(v: unknown): v is TaskSwipeAction;
|
|
14
|
+
export interface SwipeConfig {
|
|
15
|
+
threshold: number;
|
|
16
|
+
axisRatio: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const DEFAULT_SWIPE: SwipeConfig;
|
|
19
|
+
export declare function classifySwipe(start: Point, end: Point, config?: SwipeConfig): SwipeDirection;
|
|
20
|
+
export type TaskSwipeEffect = "complete" | "schedule" | "indent" | "outdent" | "delete";
|
|
21
|
+
export interface TaskSwipeRow {
|
|
22
|
+
locked: boolean;
|
|
23
|
+
isSubtask: boolean;
|
|
24
|
+
canIndent: boolean;
|
|
25
|
+
canOutdent: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function resolveTaskSwipe(action: TaskSwipeAction, row: TaskSwipeRow): TaskSwipeEffect | null;
|
|
28
|
+
export declare function isHorizontal(start: Point, end: Point, config?: SwipeConfig): boolean;
|
|
29
|
+
export declare function clampOffset(dx: number, max: number): number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Task } from "@atlastodo/client-core";
|
|
2
|
+
export interface ClipboardContext {
|
|
3
|
+
originOf?: (task: Task) => {
|
|
4
|
+
project?: string;
|
|
5
|
+
section?: string;
|
|
6
|
+
} | undefined;
|
|
7
|
+
formatDue?: (ms: number) => string;
|
|
8
|
+
formatStart?: (ms: number) => string;
|
|
9
|
+
formatRecurrence?: (rule: string) => string;
|
|
10
|
+
labelsOf?: (task: Task) => string[];
|
|
11
|
+
assigneeOf?: (task: Task) => string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serialize selected tasks to a readable clipboard block: one `- {title} (metadata)` line per task,
|
|
15
|
+
* description indented beneath. Metadata carries all user data (due, start, priority p1..p3,
|
|
16
|
+
* recurrence, estimate, labels, assignee, project/section); id-based fields appear only when the
|
|
17
|
+
* caller supplies a resolver. The single serialization source of truth.
|
|
18
|
+
*/
|
|
19
|
+
export declare function serializeTasksForClipboard(tasks: Task[], ctx?: ClipboardContext): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Task } from "@atlastodo/client-core";
|
|
2
|
+
import { type GroupBy, type GroupContext } from "./grouping.js";
|
|
3
|
+
import { type FlatTaskRow } from "./taskTree.js";
|
|
4
|
+
/**
|
|
5
|
+
* A task list's rendered shape: where {@link ./grouping} and {@link ./taskTree} are composed.
|
|
6
|
+
*
|
|
7
|
+
* Views filter tasks individually, so a matching task's parent or open subtasks are often outside
|
|
8
|
+
* the set. A group is widened before nesting with the open subtasks below its matched tasks and
|
|
9
|
+
* the parents needed to reach them; a pulled-in parent is context (`matched: false`).
|
|
10
|
+
*
|
|
11
|
+
* Grouping runs on the matched set alone, so no bucket vanishes and no matched task changes
|
|
12
|
+
* bucket. Both walks stop at a matched task, so a parent and child in different buckets cannot
|
|
13
|
+
* nest; the child renders at depth 0. Order is injected because `buildTaskTree` re-orders every
|
|
14
|
+
* level: roots take the group's order, siblings take `sortBy`.
|
|
15
|
+
*/
|
|
16
|
+
export interface ListTaskRow extends FlatTaskRow {
|
|
17
|
+
matched: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface TaskListSection {
|
|
20
|
+
key: string;
|
|
21
|
+
kind: GroupBy;
|
|
22
|
+
accent?: "danger";
|
|
23
|
+
matched: Task[];
|
|
24
|
+
tasks: Task[];
|
|
25
|
+
rows: ListTaskRow[];
|
|
26
|
+
}
|
|
27
|
+
export interface TaskListTreeOptions extends GroupContext {
|
|
28
|
+
groupBy: GroupBy;
|
|
29
|
+
allTasks?: Task[];
|
|
30
|
+
collapsedTasks?: ReadonlySet<string>;
|
|
31
|
+
}
|
|
32
|
+
export declare function taskContextClosure(sectionTasks: Task[], allTasks: Task[], matchedIds: ReadonlySet<string>): Task[];
|
|
33
|
+
export declare function taskListSections(matched: Task[], options: TaskListTreeOptions): TaskListSection[];
|