@atlastodo/shared 0.0.0-stage → 0.1.11-rc.5

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.
Files changed (67) hide show
  1. package/LICENSE +661 -0
  2. package/dist/activity.d.ts +23 -0
  3. package/dist/avatarColor.d.ts +15 -0
  4. package/dist/boardMove.d.ts +14 -0
  5. package/dist/bugReport.d.ts +47 -0
  6. package/dist/calendar.d.ts +23 -0
  7. package/dist/comments.d.ts +14 -0
  8. package/dist/countdown.d.ts +14 -0
  9. package/dist/cursor.d.ts +6 -0
  10. package/dist/dataTransfer.d.ts +19 -0
  11. package/dist/duplicateProject.d.ts +19 -0
  12. package/dist/duplicateSection.d.ts +15 -0
  13. package/dist/filterQuery.d.ts +64 -0
  14. package/dist/focus.d.ts +17 -0
  15. package/dist/format.d.ts +30 -0
  16. package/dist/fuzzy.d.ts +14 -0
  17. package/dist/grouping.d.ts +24 -0
  18. package/dist/habitGroups.d.ts +46 -0
  19. package/dist/habitSchedule.d.ts +4 -0
  20. package/dist/habits.d.ts +115 -0
  21. package/dist/hotkeys.d.ts +50 -0
  22. package/dist/i18n.d.ts +14796 -0
  23. package/dist/ids.d.ts +7 -0
  24. package/dist/importTicktick.d.ts +34 -0
  25. package/dist/importTodoist.d.ts +60 -0
  26. package/dist/index.d.ts +62 -0
  27. package/dist/index.js +21446 -0
  28. package/dist/members.d.ts +26 -0
  29. package/dist/planDay.d.ts +31 -0
  30. package/dist/pomodoro.d.ts +49 -0
  31. package/dist/priority.d.ts +2 -0
  32. package/dist/projectMapper.d.ts +15 -0
  33. package/dist/projectStyle.d.ts +14 -0
  34. package/dist/projectTree.d.ts +40 -0
  35. package/dist/quickAddLexicons.d.ts +64 -0
  36. package/dist/quickAddLexiconsWire.d.ts +11 -0
  37. package/dist/quickAddParse.d.ts +44 -0
  38. package/dist/quickSchedule.d.ts +9 -0
  39. package/dist/rank.d.ts +9 -0
  40. package/dist/recurrence.d.ts +60 -0
  41. package/dist/regions.d.ts +6 -0
  42. package/dist/reminderSchedule.d.ts +42 -0
  43. package/dist/reminders.d.ts +28 -0
  44. package/dist/reorder.d.ts +21 -0
  45. package/dist/sectionMapper.d.ts +8 -0
  46. package/dist/sectionOps.d.ts +5 -0
  47. package/dist/serverUrl.d.ts +12 -0
  48. package/dist/smartLists.d.ts +11 -0
  49. package/dist/stats.d.ts +28 -0
  50. package/dist/swipe.d.ts +29 -0
  51. package/dist/taskClipboard.d.ts +19 -0
  52. package/dist/taskListTree.d.ts +33 -0
  53. package/dist/taskMapper.d.ts +3 -0
  54. package/dist/taskOps.d.ts +49 -0
  55. package/dist/taskSearch.d.ts +11 -0
  56. package/dist/taskTree.d.ts +53 -0
  57. package/dist/theme.d.ts +14 -0
  58. package/dist/toast.d.ts +26 -0
  59. package/dist/trash.d.ts +24 -0
  60. package/dist/views.d.ts +8 -0
  61. package/dist/zonedTime.d.ts +30 -0
  62. package/package.json +55 -4
  63. package/test-vectors/filter_vectors.json +1613 -0
  64. package/test-vectors/quick_add_vectors.json +1656 -0
  65. package/test-vectors/recurrence_vectors.json +779 -0
  66. package/test-vectors/task_rules_vectors.json +729 -0
  67. package/README.md +0 -3
@@ -0,0 +1,23 @@
1
+ import type { LocalStore } from "@atlastodo/client-core";
2
+ /**
3
+ * Activity-feed entries: an `activity` entity recording a key task change (status, due date,
4
+ * assignee) with actor and timestamp, written by {@link writeActivity} and shown beside comments.
5
+ * Fans out like a comment on shared projects.
6
+ */
7
+ export type ActivityKind = "status" | "due" | "assignee";
8
+ export interface Activity {
9
+ id: string;
10
+ task_id: string;
11
+ actor_id: string;
12
+ kind: ActivityKind;
13
+ from: string | null;
14
+ to: string | null;
15
+ created_at: number;
16
+ }
17
+ export declare function toActivity(id: string, fields: Record<string, unknown>): Activity;
18
+ /**
19
+ * Append an activity entry. No-op when `actorId` is unknown (background contexts), so only
20
+ * user-driven edits are recorded. The id comes from `store.newEntityId()`: Hermes has no global
21
+ * `crypto`, and a non-UUID id would 422 the sync batch.
22
+ */
23
+ export declare function writeActivity(store: LocalStore, actorId: string | undefined, taskId: string, kind: ActivityKind, from: string | null, to: string | null, now?: number): void;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Deterministic avatar colors: a string hash maps a user id to a hue with fixed saturation and
3
+ * lightness so white text stays readable in both themes.
4
+ */
5
+ export interface AvatarColor {
6
+ background: string;
7
+ color: string;
8
+ }
9
+ /**
10
+ * The current user always renders in this fixed Atlas indigo so "me" looks the same everywhere;
11
+ * others get their hashed color, where occasional collisions are fine.
12
+ */
13
+ export declare const SELF_AVATAR_COLOR: AvatarColor;
14
+ export declare function avatarColor(id: string): AvatarColor;
15
+ export declare function avatarColorFor(id: string, selfId?: string): AvatarColor;
@@ -0,0 +1,14 @@
1
+ import type { Task } from "@atlastodo/client-core";
2
+ export interface BoardMoveWrite {
3
+ id: string;
4
+ section_id: string | null;
5
+ sort_order?: number;
6
+ }
7
+ export declare function columnCards(tasks: Task[], sectionId: string | null, excludeId?: string): Task[];
8
+ export declare function columnRoots(tasks: Task[], sectionId: string | null, excludeId?: string): Task[];
9
+ export declare function columnMoveWrites(tasks: Task[], activeId: string, toSection: string | null, toIndex: number): BoardMoveWrite[];
10
+ /**
11
+ * Move several cards to the end of one column, in the given order: {@link columnMoveWrites} per
12
+ * card against a running copy, so each lands after the previous one. The last write per task wins.
13
+ */
14
+ export declare function columnMoveManyWrites(tasks: Task[], ids: string[], toSection: string | null): BoardMoveWrite[];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Building a bug report that carries no task content, in layers:
3
+ * 1. Allowlist construction: {@link buildBugReport} never spreads; every payload field is named.
4
+ * 2. A non-`Error` throw keeps only its type name: `throw "task \"Buy flowers\" is broken"` is a
5
+ * real pattern and no string matching would be trustworthy.
6
+ * 3. {@link stackFrames} keeps only lines that look like stack frames.
7
+ * 4. {@link redactText} on every string the app did not author: quoted runs and content-shaped key
8
+ * values are replaced, then truncated.
9
+ * 5. Breadcrumb refs and routes are allowlisted ({@link sanitizeRef}): a path or UUID, else dropped.
10
+ *
11
+ * Residual gap: an `Error` message interpolating a task title unquoted survives layers 1-3 and is
12
+ * only truncated. House rule: never interpolate a task title or notes into a thrown `Error`.
13
+ */
14
+ import type { BreadcrumbCode, BugReportBreadcrumb, BugReportDiagnostics, BugReportKind, BugReportPayload } from "@atlastodo/client-core";
15
+ export declare const REPORT_LIMITS: {
16
+ readonly message: 2000;
17
+ readonly stack: 16000;
18
+ readonly description: 4000;
19
+ readonly shortField: 200;
20
+ readonly breadcrumbs: 50;
21
+ readonly frame: 200;
22
+ };
23
+ export declare function redactText(text: string, max?: number): string;
24
+ export declare function sanitizeRef(ref: string | undefined | null): string | undefined;
25
+ export declare function stackFrames(stack: string | undefined | null): string | undefined;
26
+ export interface BreadcrumbTrail {
27
+ add(code: BreadcrumbCode, ref: string | undefined, at: number): void;
28
+ list(): BugReportBreadcrumb[];
29
+ clear(): void;
30
+ }
31
+ export declare function createBreadcrumbTrail(capacity?: number): BreadcrumbTrail;
32
+ export interface BuildReportInput {
33
+ id: string;
34
+ kind: BugReportKind;
35
+ thrown: unknown;
36
+ appVersion: string;
37
+ platform: string;
38
+ osVersion?: string;
39
+ route?: string;
40
+ description?: string;
41
+ deviceId?: string;
42
+ diagnostics: BugReportDiagnostics;
43
+ breadcrumbs: BugReportBreadcrumb[];
44
+ now: number;
45
+ }
46
+ export declare function buildBugReport(input: BuildReportInput): BugReportPayload;
47
+ export declare function emptyDiagnostics(): BugReportDiagnostics;
@@ -0,0 +1,23 @@
1
+ import type { Task } from "@atlastodo/client-core";
2
+ export interface DayCell {
3
+ date: number;
4
+ day: number;
5
+ inMonth: boolean;
6
+ }
7
+ export declare function weekdayLabels(weekStartsOn: number): string[];
8
+ export declare function monthGrid(year: number, month: number, weekStartsOn: number, timeZone?: string): DayCell[][];
9
+ export declare function weekDays(anchorMs: number, weekStartsOn: number, timeZone?: string): DayCell[];
10
+ export declare function isoWeek(ms: number, timeZone?: string): number;
11
+ export declare function tasksByDay(tasks: Task[], timeZone?: string): Map<number, Task[]>;
12
+ export declare function isAllDayTask(dueAt: number, timeZone?: string): boolean;
13
+ export interface TimedTaskInfo {
14
+ task: Task;
15
+ startHour: number;
16
+ startMinute: number;
17
+ durationMin: number;
18
+ }
19
+ export interface DayTasksPartition {
20
+ allDay: Task[];
21
+ timed: TimedTaskInfo[];
22
+ }
23
+ export declare function partitionDayTasks(tasks: Task[], timeZone?: string): DayTasksPartition;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A `comment` entity synced like any other. Pure mapping only; the author id is stamped by the
3
+ * hook from the current session.
4
+ */
5
+ export interface Comment {
6
+ id: string;
7
+ task_id: string;
8
+ author_id: string;
9
+ body: string;
10
+ created_at: number;
11
+ /** When the comment was last edited (Unix ms), or null if never. */
12
+ edited_at: number | null;
13
+ }
14
+ export declare function toComment(id: string, fields: Record<string, unknown>): Comment;
@@ -0,0 +1,14 @@
1
+ export interface Countdown {
2
+ remaining: number;
3
+ overdue: boolean;
4
+ days: number;
5
+ hours: number;
6
+ minutes: number;
7
+ seconds: number;
8
+ magnitude: string;
9
+ label: string;
10
+ }
11
+ export declare function countdownTo(target: number, now: number): Countdown;
12
+ export type CountdownPresetId = "weekend" | "month_end" | "year_end";
13
+ export declare const COUNTDOWN_PRESETS: CountdownPresetId[];
14
+ export declare function presetTarget(id: CountdownPresetId, now: number, timeZone?: string): number;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Move the j/k task-list cursor over an ordered list of ids. `current` may be null or an id no
3
+ * longer in the list (completed or filtered away); both resolve to an end of the list. Movement
4
+ * clamps at the ends and does not wrap.
5
+ */
6
+ export declare function cursorStep(ids: string[], current: string | null, dir: 1 | -1): string | null;
@@ -0,0 +1,19 @@
1
+ import { type EntityKind, type LocalStore } from "@atlastodo/client-core";
2
+ export declare const PORTABLE_KINDS: EntityKind[];
3
+ export interface BundleEntity {
4
+ id: string;
5
+ fields: Record<string, unknown>;
6
+ }
7
+ export interface ExportBundle {
8
+ app: "atlas-todo";
9
+ version: 1;
10
+ exportedAt: number;
11
+ entities: Record<string, BundleEntity[]>;
12
+ }
13
+ export declare function exportData(store: LocalStore, now?: number): ExportBundle;
14
+ export declare function serializeBundle(bundle: ExportBundle): string;
15
+ export declare function bundleFilename(now?: number): string;
16
+ export declare function parseBundle(text: string): ExportBundle;
17
+ export declare function importBundle(store: LocalStore, kick: () => void, bundle: ExportBundle): {
18
+ count: number;
19
+ };
@@ -0,0 +1,19 @@
1
+ import type { EntityKind, Project, Section, Task } from "@atlastodo/client-core";
2
+ export interface EntityWrite {
3
+ kind: EntityKind;
4
+ id: string;
5
+ field: string;
6
+ value: unknown;
7
+ }
8
+ export interface DuplicateProjectPlan {
9
+ newProjectId: string;
10
+ writes: EntityWrite[];
11
+ }
12
+ export declare function pushOpenTaskCopies(writes: EntityWrite[], openTasks: Task[], projectId: string, newSectionId: (task: Task) => string | undefined, opts: {
13
+ mkId: () => string;
14
+ now: number;
15
+ }): void;
16
+ export declare function planProjectDuplicate(project: Project, sections: Section[], tasks: Task[], opts: {
17
+ mkId: () => string;
18
+ now: number;
19
+ }): DuplicateProjectPlan;
@@ -0,0 +1,15 @@
1
+ import type { Section, Task } from "@atlastodo/client-core";
2
+ import { type EntityWrite } from "./duplicateProject.js";
3
+ export interface DuplicateSectionPlan {
4
+ newSectionId: string;
5
+ writes: EntityWrite[];
6
+ }
7
+ /**
8
+ * The field writes for duplicating one section and its open tasks (completed and trashed ones
9
+ * dropped, like {@link planProjectDuplicate}). Only the section name gets "(copy)"; a sub-task
10
+ * keeps its parent only when the parent is in the copy. Pure given `mkId`/`now`.
11
+ */
12
+ export declare function planSectionDuplicate(section: Section, tasks: Task[], opts: {
13
+ mkId: () => string;
14
+ now: number;
15
+ }): DuplicateSectionPlan;
@@ -0,0 +1,64 @@
1
+ import type { Priority, Task } from "@atlastodo/client-core";
2
+ /**
3
+ * A small query language for saved filters and smart lists, e.g. `@work & due:week & p1`,
4
+ * `#home | (p1 & !due:none)`.
5
+ *
6
+ * Grammar (NOT binds tightest, then AND, then OR; adjacency means AND):
7
+ *
8
+ * or := and ( ('|' | 'or') and )*
9
+ * and := not ( ('&' | 'and')? not )*
10
+ * not := ('!' | 'not') not | term
11
+ * term := '(' or ')' | atom
12
+ * atom := pN | @label | #project | due:WHEN | due:Nd | overdue | text
13
+ *
14
+ * `due:WHEN` is today/tomorrow/week/month/weekend/next-week/overdue/none; `due:Nd` is the next N
15
+ * days. `parse` never throws: invalid input returns `{ ok: false, error, at }`.
16
+ */
17
+ export type DueWhen = "today" | "tomorrow" | "week" | "month" | "weekend" | "next-week" | "overdue" | "none";
18
+ export type FilterNode = {
19
+ type: "and";
20
+ left: FilterNode;
21
+ right: FilterNode;
22
+ } | {
23
+ type: "or";
24
+ left: FilterNode;
25
+ right: FilterNode;
26
+ } | {
27
+ type: "not";
28
+ child: FilterNode;
29
+ } | {
30
+ type: "priority";
31
+ value: Priority;
32
+ } | {
33
+ type: "label";
34
+ name: string;
35
+ } | {
36
+ type: "project";
37
+ name: string;
38
+ } | {
39
+ type: "due";
40
+ when: DueWhen;
41
+ } | {
42
+ type: "dueWithin";
43
+ days: number;
44
+ } | {
45
+ type: "text";
46
+ value: string;
47
+ };
48
+ export type ParseResult = {
49
+ ok: true;
50
+ ast: FilterNode;
51
+ } | {
52
+ ok: false;
53
+ error: string;
54
+ at: number;
55
+ };
56
+ export declare function parse(input: string): ParseResult;
57
+ export interface EvalContext {
58
+ now: number;
59
+ labelsOf: (task: Task) => string[];
60
+ projectNameOf: (task: Task) => string | null;
61
+ timeZone?: string;
62
+ weekStartsOn?: number;
63
+ }
64
+ export declare function evaluate(node: FilterNode, task: Task, ctx: EvalContext): boolean;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Focus (pomodoro) session records: pure helpers over `focus_session` entities that log tracked
3
+ * time against tasks.
4
+ */
5
+ export interface FocusSession {
6
+ id: string;
7
+ task_id: string;
8
+ started_at: number;
9
+ ended_at: number;
10
+ /** Tracked focused duration in ms (less than ended - started if paused). */
11
+ duration_ms: number;
12
+ created_at: number;
13
+ }
14
+ export declare function toFocusSession(id: string, fields: Record<string, unknown>): FocusSession;
15
+ export declare function trackedMsForTask(sessions: FocusSession[], taskId: string): number;
16
+ /** Format a millisecond duration compactly as `Hh Mm` / `Mm` / `<1m`. */
17
+ export declare function formatDuration(ms: number): string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Locale, timezone and format-aware date, time and number formatting: the single place display
3
+ * strings are produced so language, timezone, 12/24h and date-format preferences apply everywhere.
4
+ * Pure; `makeFormatters(config)` builds the `Intl` formatters.
5
+ */
6
+ export type TimeFormatPref = "auto" | "12h" | "24h";
7
+ export type DateFormatPref = "auto" | "short" | "medium" | "long";
8
+ export declare function isTimeFormat(v: unknown): v is TimeFormatPref;
9
+ export declare function isDateFormat(v: unknown): v is DateFormatPref;
10
+ /**
11
+ * Resolve the BCP-47 formatting locale: the region preference wins (an English UI can format dates
12
+ * as en-GB), then the UI language, then the device default (undefined). Blank means "follow the
13
+ * device".
14
+ */
15
+ export declare function resolveLocale(region?: string, language?: string): string | undefined;
16
+ export interface FormatConfig {
17
+ locale?: string;
18
+ timeZone?: string;
19
+ timeFormat?: TimeFormatPref;
20
+ dateFormat?: DateFormatPref;
21
+ }
22
+ export interface Formatters {
23
+ dueChip(ms: number): string;
24
+ time(ms: number): string;
25
+ dateTime(ms: number): string;
26
+ date(ms: number): string;
27
+ monthYear(year: number, month: number): string;
28
+ number(n: number): string;
29
+ }
30
+ export declare function makeFormatters(config?: FormatConfig): Formatters;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tiny fuzzy matcher for the command palette. Pure and DOM-free so ranking is unit-testable.
3
+ * A query matches if its characters appear in order (subsequence) in the item's text; contiguous
4
+ * and early matches score higher. An empty query keeps every item in its original order.
5
+ */
6
+ export interface FuzzyItem {
7
+ label: string;
8
+ /** Extra searchable text (synonyms, group name) that doesn't show in the label. */
9
+ keywords?: string;
10
+ }
11
+ /** Score a subsequence match of `query` against `text`, or null if not all chars appear in order. */
12
+ export declare function scoreMatch(text: string, query: string): number | null;
13
+ /** Filter and rank items by a fuzzy query, best first. Stable for equal scores. */
14
+ export declare function filterActions<T extends FuzzyItem>(query: string, items: T[]): T[];
@@ -0,0 +1,24 @@
1
+ import type { Task } from "@atlastodo/client-core";
2
+ export type GroupBy = "none" | "date" | "priority" | "label" | "project" | "completed";
3
+ export type SortBy = "manual" | "due" | "priority" | "alpha" | "created" | "modified";
4
+ export declare const GROUP_BYS: GroupBy[];
5
+ export declare const SORT_BYS: SortBy[];
6
+ export declare function isGroupBy(v: unknown): v is GroupBy;
7
+ export declare function isSortBy(v: unknown): v is SortBy;
8
+ export interface TaskGroup {
9
+ key: string;
10
+ kind: GroupBy;
11
+ accent?: "danger";
12
+ tasks: Task[];
13
+ }
14
+ export declare function taskComparator(sortBy: SortBy): (a: Task, b: Task) => number;
15
+ export declare function sortTasks(tasks: Task[], sortBy: SortBy): Task[];
16
+ export interface GroupContext {
17
+ now: number;
18
+ timeZone?: string;
19
+ sortBy: SortBy;
20
+ projectOrder: string[];
21
+ labelOrder: string[];
22
+ }
23
+ export declare function completedBucket(completedAt: number, now: number, timeZone?: string): string;
24
+ export declare function groupTasks(tasks: Task[], groupBy: GroupBy, ctx: GroupContext): TaskGroup[];
@@ -0,0 +1,46 @@
1
+ import { type CheckinStates, type CompletionRate, type Habit } from "./habits.js";
2
+ import { hiddenProjectIds } from "./projectTree.js";
3
+ export type HabitListRow = {
4
+ kind: "group";
5
+ key: string;
6
+ group: Habit;
7
+ memberCount: number;
8
+ expanded: boolean;
9
+ } | {
10
+ kind: "habit";
11
+ key: string;
12
+ habit: Habit;
13
+ depth: 0 | 1;
14
+ };
15
+ export declare const hiddenHabitIds: typeof hiddenProjectIds;
16
+ export declare function habitGroupMembers(habits: Habit[], groupId: string): Habit[];
17
+ export declare function habitSiblings(habits: Habit[], habit: Habit): Habit[];
18
+ export declare function flattenHabitGroups(habits: Habit[], isExpanded?: (groupId: string) => boolean): HabitListRow[];
19
+ export declare function moveHabitTarget(habits: Habit[], id: string, groupId: string | null): {
20
+ id: string;
21
+ parent_id: string | null;
22
+ sort_order: number;
23
+ } | null;
24
+ export interface HabitGroupMember {
25
+ habit: Habit;
26
+ states: CheckinStates;
27
+ }
28
+ export interface HabitGroupDay {
29
+ date: string;
30
+ due: number;
31
+ done: number;
32
+ excluded: boolean;
33
+ met: boolean;
34
+ inProgress: boolean;
35
+ missing: string[];
36
+ }
37
+ export declare function habitGroupDays(members: readonly HabitGroupMember[], fromKey: string, toKey: string, todayKey: string, weekStartsOn?: number): HabitGroupDay[];
38
+ export declare function habitGroupStreak(members: readonly HabitGroupMember[], todayMs: number, weekStartsOn?: number): number;
39
+ export declare function habitGroupBestStreak(members: readonly HabitGroupMember[], todayMs: number, weekStartsOn?: number): number;
40
+ export interface HabitGroupBreak {
41
+ date: string;
42
+ missing: string[];
43
+ }
44
+ export declare function habitGroupBreak(members: readonly HabitGroupMember[], todayMs: number, weekStartsOn?: number): HabitGroupBreak | null;
45
+ export declare function habitGroupToday(members: readonly HabitGroupMember[], todayMs: number, weekStartsOn?: number): HabitGroupDay | null;
46
+ export declare function habitGroupRate(members: readonly HabitGroupMember[], fromMs: number, toMs: number, weekStartsOn?: number): CompletionRate;
@@ -0,0 +1,4 @@
1
+ import { type CheckinStates, type Habit } from "./habits.js";
2
+ import type { ScheduledReminder } from "./reminderSchedule.js";
3
+ export declare function habitReminderId(habitId: string, date: string): string;
4
+ export declare function habitReminders(habits: Habit[], statesOf: (habitId: string) => CheckinStates, now: number, weekStartsOn?: number, horizonDays?: number): ScheduledReminder[];
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Habits and streaks: pure, date-based logic with an injected `now`.
3
+ *
4
+ * Check-ins are keyed by local calendar date, ignoring the `timezone` preference (re-keying would
5
+ * rewrite streaks). Date arithmetic uses the UTC calendar helpers, which are DST-safe.
6
+ *
7
+ * Every goal kind reduces to periods wanting `target` check-ins: `daily` (one per day, `days` picks
8
+ * weekdays), `weekly` (aligned to `week_starts_on`, so changing it redraws past weeks), `interval`
9
+ * (`target`-day blocks tiled from creation).
10
+ *
11
+ * A short in-progress period neither counts nor breaks a streak. Skipped days shrink the target;
12
+ * a period whose target falls to 0 is dropped and bridged.
13
+ */
14
+ export type HabitGoalKind = "daily" | "weekly" | "interval";
15
+ export type HabitKind = "habit" | "group";
16
+ export type CheckinState = "done" | "skip";
17
+ export interface HabitSchedule {
18
+ from: string;
19
+ goal_kind: HabitGoalKind;
20
+ days: number[];
21
+ target: number;
22
+ }
23
+ export interface Habit {
24
+ id: string;
25
+ name: string;
26
+ kind: HabitKind;
27
+ parent_id: string | null;
28
+ goal_kind: HabitGoalKind;
29
+ days: number[];
30
+ target: number;
31
+ color: string;
32
+ icon: string;
33
+ notes: string;
34
+ steps: string[];
35
+ unit: string;
36
+ reminder_time: string | null;
37
+ schedule_history: HabitSchedule[];
38
+ archived_at: number | null;
39
+ created_at: number;
40
+ sort_order: number;
41
+ }
42
+ export interface HabitCheckin {
43
+ id: string;
44
+ habit_id: string;
45
+ date: string;
46
+ state: CheckinState;
47
+ value: number;
48
+ note: string;
49
+ created_at: number;
50
+ }
51
+ export type CheckinStates = ReadonlyMap<string, CheckinState>;
52
+ export declare const MAX_SCAN_DAYS: number;
53
+ export declare const MAX_STEPS = 50;
54
+ export declare const MAX_SCHEDULE_HISTORY = 200;
55
+ export declare function dateKey(d: Date): string;
56
+ export declare function dateKeyFromMs(ms: number): string;
57
+ export declare function shiftDateKey(key: string, days: number): string;
58
+ export declare function daysBetweenKeys(a: string, b: string): number;
59
+ export declare function dateKeyToMs(key: string): number;
60
+ export declare function weekdayOfKey(key: string): number;
61
+ export declare function startOfWeekKey(key: string, weekStartsOn: number): string;
62
+ export declare function toHabit(id: string, fields: Record<string, unknown>): Habit;
63
+ export declare function toHabitCheckin(id: string, fields: Record<string, unknown>): HabitCheckin;
64
+ export declare function isBackfilled(checkin: HabitCheckin): boolean;
65
+ type GoalShape = Pick<Habit, "goal_kind" | "days" | "target">;
66
+ export declare function isScheduledOn(habit: Habit, key: string, weekStartsOn?: number): boolean;
67
+ export declare function goalFrequency(habit: GoalShape): number;
68
+ export interface ScheduleSegment {
69
+ schedule: HabitSchedule;
70
+ from: string;
71
+ until: string | null;
72
+ }
73
+ export declare function scheduleSegments(habit: Habit, weekStartsOn?: number): ScheduleSegment[];
74
+ export declare function effectiveSchedule(habit: Habit, key: string, weekStartsOn?: number): HabitSchedule;
75
+ export declare function appendScheduleChange(habit: Habit, patch: Partial<GoalShape>, nowMs: number): HabitSchedule[] | null;
76
+ export interface HabitPeriod {
77
+ from: string;
78
+ to: string;
79
+ scheduled: string[];
80
+ goal: number;
81
+ }
82
+ export interface PeriodResult {
83
+ period: HabitPeriod;
84
+ done: number;
85
+ skipped: number;
86
+ target: number;
87
+ met: boolean;
88
+ inProgress: boolean;
89
+ }
90
+ export declare function periodsBetween(habit: Habit, fromKey: string, toKey: string, weekStartsOn?: number): HabitPeriod[];
91
+ export declare function evaluatePeriods(states: CheckinStates, periods: HabitPeriod[], todayKey: string): PeriodResult[];
92
+ export declare function currentStreak(habit: Habit, states: CheckinStates, todayMs: number, weekStartsOn?: number): number;
93
+ export declare function currentPeriod(habit: Habit, states: CheckinStates, todayMs: number, weekStartsOn?: number): PeriodResult | null;
94
+ export declare function isActiveToday(habit: Habit, states: CheckinStates, todayMs: number, weekStartsOn?: number): boolean;
95
+ export declare function bestStreak(habit: Habit, states: CheckinStates, todayMs: number, weekStartsOn?: number): number;
96
+ export interface CompletionRate {
97
+ periods: number;
98
+ met: number;
99
+ rate: number;
100
+ }
101
+ export declare function completionRate(habit: Habit, states: CheckinStates, fromMs: number, toMs: number, weekStartsOn?: number): CompletionRate;
102
+ export declare function totalCheckins(states: CheckinStates): number;
103
+ export declare function nextDue(habit: Habit, states: CheckinStates, todayMs: number, weekStartsOn?: number): string | null;
104
+ export interface StrengthPoint {
105
+ date: string;
106
+ score: number;
107
+ }
108
+ /**
109
+ * Loop Habit Tracker's strength curve: an exponential moving average that dips on a miss but never
110
+ * resets. `multiplier = 0.5 ** (sqrt(frequency) / 13)` per day, raised to the period length.
111
+ * Skipped, off-schedule and short in-progress periods carry the score forward.
112
+ */
113
+ export declare function strengthSeries(habit: Habit, states: CheckinStates, fromMs: number, toMs: number, weekStartsOn?: number): StrengthPoint[];
114
+ export declare function currentStrength(habit: Habit, states: CheckinStates, todayMs: number, weekStartsOn?: number): number;
115
+ export {};
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Keyboard-shortcut dispatch: pure and DOM-free. [`HOTKEY_BINDINGS`] is the single source of truth,
3
+ * walked by `dispatchHotkey` and rendered by the help UIs.
4
+ *
5
+ * Cmd/Ctrl-K fires even while typing; every other binding is inert then, and any modifier
6
+ * suppresses single-key shortcuts. A missing or declining (`false`) handler reports unhandled so the
7
+ * browser default survives. Escape clears a multi-selection but never reports handled, so an open
8
+ * dialog still closes.
9
+ */
10
+ export interface HotkeyInput {
11
+ key: string;
12
+ metaKey: boolean;
13
+ ctrlKey: boolean;
14
+ altKey: boolean;
15
+ typing: boolean;
16
+ }
17
+ export type HotkeyHandler = () => void | boolean;
18
+ export interface HotkeyHandlers {
19
+ openPalette?: HotkeyHandler;
20
+ focusSearch?: HotkeyHandler;
21
+ openHelp?: HotkeyHandler;
22
+ focusQuickAdd?: HotkeyHandler;
23
+ cursorNext?: HotkeyHandler;
24
+ cursorPrev?: HotkeyHandler;
25
+ openCursor?: HotkeyHandler;
26
+ completeCursor?: HotkeyHandler;
27
+ rescheduleCursor?: HotkeyHandler;
28
+ deleteCursor?: HotkeyHandler;
29
+ clearSelection?: HotkeyHandler;
30
+ selectAll?: HotkeyHandler;
31
+ copySelection?: HotkeyHandler;
32
+ duplicateSelection?: HotkeyHandler;
33
+ cutSelection?: HotkeyHandler;
34
+ }
35
+ export type HotkeyGroup = "global" | "navigation" | "taskActions" | "selection";
36
+ export declare const HOTKEY_GROUPS: readonly HotkeyGroup[];
37
+ export interface HotkeyBinding {
38
+ action: keyof HotkeyHandlers;
39
+ triggerKeys: string[];
40
+ displayKeys: string[];
41
+ descriptionKey: string;
42
+ group: HotkeyGroup;
43
+ mod?: boolean;
44
+ /** Fires even while focus is in a text field (only Cmd/Ctrl-K). */
45
+ whileTyping?: boolean;
46
+ /** Fires, but reports unhandled so the event is not preventDefaulted (Escape). */
47
+ noPreventDefault?: boolean;
48
+ }
49
+ export declare const HOTKEY_BINDINGS: readonly HotkeyBinding[];
50
+ export declare function dispatchHotkey(input: HotkeyInput, h: HotkeyHandlers): boolean;