Skip to content

Commit 8e5ef49

Browse files
Dates
1 parent 137c926 commit 8e5ef49

File tree

13 files changed

+612
-41
lines changed

13 files changed

+612
-41
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@tiptap/extension-task-list": "^2.11.2",
2626
"@tiptap/pm": "^2.11.2",
2727
"@tiptap/starter-kit": "^2.11.2",
28+
"chrono-node": "^2.9.0",
2829
"phosphor-svelte": "^3.0.1",
2930
"tiptap-markdown": "^0.8.10"
3031
}

src/App.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747
}
4848
}, 1000)
4949
50+
// Reset scroll when zoom changes
51+
$: if ($zoomedItemId || $zoomedItemId === null) {
52+
if (containerRef) {
53+
containerRef.scrollTop = 0;
54+
}
55+
}
56+
5057
items.subscribe(() => {
5158
if (isInitialLoad) {
5259
isInitialLoad = false
@@ -697,6 +704,7 @@
697704
item={$filteredItems.tree}
698705
isTop={true}
699706
highlightPhrase={$highlightPhrase}
707+
activeZoomedItemId={$zoomedItemId}
700708
/>
701709
</div>
702710
</div>

src/components/AutocompleteMenu.svelte

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script>
22
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
3+
import { formatRelativeTime, formatAbsoluteDate } from '../utils/dateParser.js'
34
45
export let items = []
56
export let coords = { top: 0, left: 0 }
67
export let query = ''
78
export let visible = false
9+
export let dateItem = null
810
911
const dispatch = createEventDispatcher()
1012
@@ -25,8 +27,21 @@
2527
return text?.toLowerCase().includes(query.toLowerCase())
2628
})
2729
28-
$: if (filteredItems.length > 0 && selectedIndex >= filteredItems.length) {
29-
selectedIndex = filteredItems.length - 1
30+
$: dateDisplayItem = dateItem ? {
31+
_isDate: true,
32+
date: dateItem.date,
33+
relativeText: formatRelativeTime(dateItem.date),
34+
absoluteText: formatAbsoluteDate(dateItem.date)
35+
} : null
36+
37+
$: allDisplayItems = dateDisplayItem
38+
? [dateDisplayItem, ...filteredItems]
39+
: filteredItems
40+
41+
$: totalLength = allDisplayItems.length
42+
43+
$: if (totalLength > 0 && selectedIndex >= totalLength) {
44+
selectedIndex = totalLength - 1
3045
}
3146
3247
function handleKeyDown(event) {
@@ -35,16 +50,16 @@
3550
if (event.key === 'ArrowDown') {
3651
event.preventDefault()
3752
event.stopPropagation()
38-
selectedIndex = (selectedIndex + 1) % filteredItems.length
53+
selectedIndex = (selectedIndex + 1) % totalLength
3954
} else if (event.key === 'ArrowUp') {
4055
event.preventDefault()
4156
event.stopPropagation()
42-
selectedIndex = (selectedIndex - 1 + filteredItems.length) % filteredItems.length
57+
selectedIndex = (selectedIndex - 1 + totalLength) % totalLength
4358
} else if (event.key === 'Enter' || event.key === 'Tab') {
44-
if (filteredItems.length > 0) {
59+
if (totalLength > 0) {
4560
event.preventDefault()
4661
event.stopPropagation()
47-
selectItem(filteredItems[selectedIndex])
62+
selectItem(allDisplayItems[selectedIndex])
4863
}
4964
} else if (event.key === 'Escape') {
5065
event.preventDefault()
@@ -66,22 +81,37 @@
6681
})
6782
</script>
6883
69-
{#if visible && filteredItems.length > 0}
84+
{#if visible && totalLength > 0}
7085
<div
7186
bind:this={menuElement}
7287
class="autocomplete-menu"
7388
style="top: {coords.bottom + 4}px; left: {coords.left}px;"
7489
>
75-
{#each filteredItems as item, index}
76-
<button
77-
type="button"
78-
class="autocomplete-item"
79-
class:selected={index === selectedIndex}
80-
on:click={() => selectItem(item)}
81-
on:mouseenter={() => selectedIndex = index}
82-
>
83-
{truncateText(getItemText(item))}
84-
</button>
90+
{#each allDisplayItems as item, index}
91+
{#if item._isDate}
92+
<button
93+
type="button"
94+
class="autocomplete-item date-item"
95+
class:selected={index === selectedIndex}
96+
on:click={() => selectItem(item)}
97+
on:mouseenter={() => selectedIndex = index}
98+
title={item.absoluteText}
99+
>
100+
<span class="date-icon">📅</span>
101+
<span class="date-relative">{item.relativeText}</span>
102+
<span class="date-absolute">{item.absoluteText}</span>
103+
</button>
104+
{:else}
105+
<button
106+
type="button"
107+
class="autocomplete-item"
108+
class:selected={index === selectedIndex}
109+
on:click={() => selectItem(item)}
110+
on:mouseenter={() => selectedIndex = index}
111+
>
112+
{truncateText(getItemText(item))}
113+
</button>
114+
{/if}
85115
{/each}
86116
</div>
87117
{/if}
@@ -95,7 +125,7 @@
95125
border-radius: 6px;
96126
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
97127
min-width: 140px;
98-
max-width: 260px;
128+
max-width: 280px;
99129
max-height: 180px;
100130
overflow-y: auto;
101131
padding: 3px;
@@ -121,4 +151,29 @@
121151
background: rgba(0, 0, 0, 0.05);
122152
color: #333;
123153
}
154+
155+
.autocomplete-item.date-item {
156+
display: flex;
157+
align-items: center;
158+
gap: 6px;
159+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
160+
margin-bottom: 2px;
161+
padding-bottom: 8px;
162+
}
163+
164+
.date-icon {
165+
font-size: 12px;
166+
flex-shrink: 0;
167+
}
168+
169+
.date-relative {
170+
font-weight: 500;
171+
color: var(--accent, #49baf2);
172+
}
173+
174+
.date-absolute {
175+
font-size: 11px;
176+
color: #999;
177+
margin-left: auto;
178+
}
124179
</style>

src/components/Item.svelte

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { itemStore } from '../stores/itemStore.js'
88
import { parseStatusPrefix } from '../utils/serializer.js'
99
import { focusItem, focusDescription } from '../utils/focus.js'
10+
import { send, receive } from '../utils/transitions.js'
1011
1112
export let item
1213
export let isSelected = false
@@ -21,6 +22,7 @@
2122
2223
$: hasDescription = !!item.description?.trim()
2324
$: hasChildren = item.children?.length > 0
25+
2426
$: editorValue = item.hasCheckbox
2527
? (item.completed ? '[x] ' : '[ ] ') + (item.text || '')
2628
: (item.text || '')
@@ -116,6 +118,10 @@
116118
itemStore.navigateToItem(event.detail.id)
117119
}
118120
121+
function handleDateClick(event) {
122+
itemStore.setSearch(event.detail.searchStr)
123+
}
124+
119125
function handleTextChange(event) {
120126
const rawText = event.detail.value
121127
const statusInfo = parseStatusPrefix(rawText)
@@ -246,7 +252,11 @@
246252
background={hasChildren && !item.open}
247253
on:click={handleZoom}
248254
/>
249-
<div class="title-editor">
255+
<div
256+
class="title-editor"
257+
in:receive={{key: 'title_' + item.id}}
258+
out:send={{key: 'title_' + item.id}}
259+
>
250260
<RichEditor
251261
bind:this={titleEditorRef}
252262
value={editorValue}
@@ -270,12 +280,17 @@
270280
on:description={handleShowDescription}
271281
on:hashtagclick={handleHashtagClick}
272282
on:itemrefclick={handleItemRefClick}
283+
on:dateclick={handleDateClick}
273284
/>
274285
</div>
275286
</div>
276287
277288
{#if hasDescription || showDescriptionEditor}
278-
<div class="description-editor">
289+
<div
290+
class="description-editor"
291+
in:receive={{key: 'desc_' + item.id}}
292+
out:send={{key: 'desc_' + item.id}}
293+
>
279294
<RichEditor
280295
bind:value={item.description}
281296
isDescription={true}
@@ -288,6 +303,7 @@
288303
on:togglecomplete={handleToggleComplete}
289304
on:hashtagclick={handleHashtagClick}
290305
on:itemrefclick={handleItemRefClick}
306+
on:dateclick={handleDateClick}
291307
/>
292308
</div>
293309
{/if}
@@ -378,8 +394,4 @@
378394
min-width: 0;
379395
}
380396
381-
.description-editor {
382-
margin-left: 1.3rem;
383-
font-size: 0.85rem;
384-
}
385397
</style>

src/components/List.svelte

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { parseStatusPrefix } from '../utils/serializer.js'
88
import { focusItem, focusDescription, getItemAbove, getItemBelow, flattenVisible } from '../utils/focus.js'
99
import { get } from 'svelte/store'
10+
import { send, receive } from '../utils/transitions.js'
11+
import { flip } from 'svelte/animate'
1012
1113
function handleHashtagClick(event) {
1214
itemStore.setSearch(event.detail.hashtag)
@@ -16,20 +18,25 @@
1618
itemStore.navigateToItem(event.detail.id)
1719
}
1820
21+
function handleDateClick(event) {
22+
itemStore.setSearch(event.detail.searchStr)
23+
}
24+
1925
export let item
2026
export let isTop = false
2127
export let outermost = false
2228
export let highlightPhrase = null
29+
export let activeZoomedItemId = null
2330
2431
const dispatch = createEventDispatcher()
2532
26-
const { zoomedItemId, selection, selectionAnchor, selectionDirection } = itemStore
33+
const { selection, selectionAnchor, selectionDirection, transitionMode } = itemStore
2734
2835
let containerElement
2936
let showDescriptionEditor = false
3037
let zoomedTitleEditorRef
3138
32-
$: isZoomedRoot = isTop && !!$zoomedItemId && item.id === $zoomedItemId
39+
$: isZoomedRoot = isTop && !!activeZoomedItemId && item.id === activeZoomedItemId
3340
$: hasDescription = !!item.description?.trim()
3441
$: titleEditorValue = item.hasCheckbox
3542
? (item.completed ? '[x] ' : '[ ] ') + (item.text || '')
@@ -308,8 +315,13 @@
308315
class:outermost
309316
>
310317
{#if isZoomedRoot}
311-
{#key item.id}
312-
<h1 class="zoomed_title" class:completed={item.completed} id="item_{item.id}">
318+
<h1
319+
class="zoomed_title"
320+
class:completed={item.completed}
321+
id="item_{item.id}"
322+
in:receive={{key: 'title_' + item.id}}
323+
out:send={{key: 'title_' + item.id}}
324+
>
313325
<RichEditor
314326
bind:this={zoomedTitleEditorRef}
315327
value={titleEditorValue}
@@ -322,6 +334,7 @@
322334
on:change={handleTitleTextChange}
323335
on:hashtagclick={handleHashtagClick}
324336
on:itemrefclick={handleItemRefClick}
337+
on:dateclick={handleDateClick}
325338
on:togglecomplete={handleTitleToggleComplete}
326339
on:checkboxtoggle={handleTitleCheckboxToggle}
327340
on:checkboxremoved={handleTitleCheckboxRemoved}
@@ -330,7 +343,11 @@
330343
</h1>
331344
332345
{#if hasDescription || showDescriptionEditor}
333-
<div class="zoomed_description">
346+
<div
347+
class="zoomed_description"
348+
in:receive={{key: 'desc_' + item.id}}
349+
out:send={{key: 'desc_' + item.id}}
350+
>
334351
<RichEditor
335352
bind:value={item.description}
336353
isDescription={true}
@@ -345,14 +362,23 @@
345362
/>
346363
</div>
347364
{:else}
348-
<button class="add-description-btn" on:click={handleDescriptionClick}>
365+
<button
366+
class="add-description-btn"
367+
on:click={handleDescriptionClick}
368+
in:receive={{key: 'desc_' + item.id}}
369+
out:send={{key: 'desc_' + item.id}}
370+
>
349371
Click to add description...
350372
</button>
351373
{/if}
352-
{/key}
353374
{:else if isTop && item.text?.length && !outermost}
354-
{#key item.id}
355-
<h2 class="item_title" class:completed={item.completed} id="item_{item.id}">
375+
<h2
376+
class="item_title"
377+
class:completed={item.completed}
378+
id="item_{item.id}"
379+
in:receive={{key: 'title_' + item.id}}
380+
out:send={{key: 'title_' + item.id}}
381+
>
356382
<RichEditor
357383
value={titleEditorValue}
358384
{highlightPhrase}
@@ -363,19 +389,22 @@
363389
on:change={handleTitleTextChange}
364390
on:hashtagclick={handleHashtagClick}
365391
on:itemrefclick={handleItemRefClick}
392+
on:dateclick={handleDateClick}
366393
on:togglecomplete={handleTitleToggleComplete}
367394
on:checkboxtoggle={handleTitleCheckboxToggle}
368395
on:checkboxremoved={handleTitleCheckboxRemoved}
369396
on:checkboxadded={handleTitleCheckboxAdded}
370397
/>
371-
</h2>
372-
{/key}
398+
</h2>
373399
{/if}
374400
375401
{#if item.children?.length && (item.open || isTop)}
376-
<ul class:children={!isTop} on:click={handleEmptyAreaClick}>
402+
<ul class:children={!isTop} on:click={handleEmptyAreaClick} on:keydown={() => {}}>
377403
{#each item.children as child (child.id)}
378-
<div class="item-row">
404+
<div
405+
class="item-row"
406+
animate:flip={{duration: $transitionMode === 'move' ? 300 : 0}}
407+
>
379408
<Item
380409
item={child}
381410
isSelected={$selection.has(child.id)}

0 commit comments

Comments
 (0)