@jungjaehoon/mama-os 0.62.0 → 0.63.0
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/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/dist/api/viewer-data.d.ts +77 -0
- package/dist/api/viewer-data.d.ts.map +1 -1
- package/dist/api/viewer-data.js +116 -0
- package/dist/api/viewer-data.js.map +1 -1
- package/dist/api/viewer-server.d.ts +1 -1
- package/dist/api/viewer-server.d.ts.map +1 -1
- package/dist/api/viewer-server.js +87 -3
- package/dist/api/viewer-server.js.map +1 -1
- package/dist/api/work-actions.d.ts +2 -0
- package/dist/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +4 -5
- package/dist/api/work-actions.js.map +1 -1
- package/dist/runtime/action-surface.d.ts +1 -1
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +3 -0
- package/dist/runtime/action-surface.js.map +1 -1
- package/dist/runtime/turn-orders.d.ts.map +1 -1
- package/dist/runtime/turn-orders.js +6 -2
- package/dist/runtime/turn-orders.js.map +1 -1
- package/package.json +2 -2
- package/public/viewer/js/modules/graph.js +169 -957
- package/public/viewer/js/modules/memory-timeline.js +215 -0
- package/public/viewer/js/utils/api.js +7 -26
- package/public/viewer/src/modules/graph.ts +185 -1132
- package/public/viewer/src/modules/memory-timeline.ts +272 -0
- package/public/viewer/src/types/global.d.ts +1 -1
- package/public/viewer/src/utils/api.ts +43 -47
- package/public/viewer/viewer.css +257 -7
- package/public/viewer/viewer.html +68 -292
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Timeline Module - what was saved, and when
|
|
3
|
+
* @module modules/memory-timeline
|
|
4
|
+
*
|
|
5
|
+
* The memory view's list: a period (today, 7 days, 30 days or one day), then by day, then by
|
|
6
|
+
* kind, then by work item and its updates. Every filter is a server read, so the list and the
|
|
7
|
+
* counts on the kind chips always describe the same records.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* eslint-env browser */
|
|
11
|
+
|
|
12
|
+
import { escapeAttr, escapeHtml, debounce, getElementByIdOrNull } from '../utils/dom.js';
|
|
13
|
+
import {
|
|
14
|
+
API,
|
|
15
|
+
type MemoryTimelineResponse,
|
|
16
|
+
type TimelineGroup,
|
|
17
|
+
type TimelineItem,
|
|
18
|
+
type TimelineRecord,
|
|
19
|
+
} from '../utils/api.js';
|
|
20
|
+
|
|
21
|
+
/** The groups the server names first, in its order, with the words and colours the list shows. */
|
|
22
|
+
const GROUPS: Record<string, { label: string; hint: string; color: string }> = {
|
|
23
|
+
owner_rule: {
|
|
24
|
+
label: 'Owner rules',
|
|
25
|
+
hint: 'Rules the owner set in their own chat',
|
|
26
|
+
color: '#FB7185',
|
|
27
|
+
},
|
|
28
|
+
learned: {
|
|
29
|
+
label: 'Learned rules',
|
|
30
|
+
hint: 'Lessons, preferences, constraints and workflows learned in other turns',
|
|
31
|
+
color: '#FBBF24',
|
|
32
|
+
},
|
|
33
|
+
decision: { label: 'Decisions', hint: 'Decisions recorded', color: '#60A5FA' },
|
|
34
|
+
fact: { label: 'Facts', hint: 'Facts recorded', color: '#2DD4BF' },
|
|
35
|
+
work: { label: 'Work updates', hint: 'Work items opened or updated', color: '#FB923C' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const VIA_LABELS: Record<string, string> = {
|
|
39
|
+
owner_chat: 'owner chat',
|
|
40
|
+
source_delta: 'source update',
|
|
41
|
+
report: 'report',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const OPERATION_LABELS: Record<string, string> = {
|
|
45
|
+
create: 'opened',
|
|
46
|
+
revise: 'updated',
|
|
47
|
+
withdraw: 'withdrawn',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function groupInfo(group: string): { label: string; hint: string; color: string } {
|
|
51
|
+
return GROUPS[group] ?? { label: group, hint: `Records of kind ${group}`, color: '#CBD5E1' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dayLabel(day: string): string {
|
|
55
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
56
|
+
weekday: 'short',
|
|
57
|
+
year: 'numeric',
|
|
58
|
+
month: 'short',
|
|
59
|
+
day: 'numeric',
|
|
60
|
+
timeZone: 'UTC',
|
|
61
|
+
}).format(new Date(`${day}T00:00:00Z`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class MemoryTimelineModule {
|
|
65
|
+
period = '7d';
|
|
66
|
+
day: string | null = null;
|
|
67
|
+
query = '';
|
|
68
|
+
groups = new Set<string>();
|
|
69
|
+
selectedId: string | null = null;
|
|
70
|
+
private request = 0;
|
|
71
|
+
private bound = false;
|
|
72
|
+
private debouncedLoad = debounce(() => void this.load(), 300);
|
|
73
|
+
|
|
74
|
+
constructor(private readonly onSelect: (id: string) => void) {}
|
|
75
|
+
|
|
76
|
+
/** Wire the controls once, then read the current period. */
|
|
77
|
+
async init(): Promise<void> {
|
|
78
|
+
if (!this.bound) {
|
|
79
|
+
this.bind();
|
|
80
|
+
this.bound = true;
|
|
81
|
+
}
|
|
82
|
+
await this.load();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private bind(): void {
|
|
86
|
+
document.querySelectorAll<HTMLButtonElement>('[data-memory-period]').forEach((button) => {
|
|
87
|
+
button.addEventListener('click', () => {
|
|
88
|
+
this.period = button.dataset.memoryPeriod ?? '7d';
|
|
89
|
+
this.day = null;
|
|
90
|
+
const dayInput = getElementByIdOrNull<HTMLInputElement>('memory-day');
|
|
91
|
+
if (dayInput) dayInput.value = '';
|
|
92
|
+
void this.load();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
getElementByIdOrNull<HTMLInputElement>('memory-day')?.addEventListener('change', (event) => {
|
|
96
|
+
const value = (event.target as HTMLInputElement).value;
|
|
97
|
+
this.day = value === '' ? null : value;
|
|
98
|
+
void this.load();
|
|
99
|
+
});
|
|
100
|
+
getElementByIdOrNull<HTMLInputElement>('memory-timeline-search')?.addEventListener(
|
|
101
|
+
'input',
|
|
102
|
+
(event) => {
|
|
103
|
+
this.query = (event.target as HTMLInputElement).value.trim();
|
|
104
|
+
this.debouncedLoad();
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
getElementByIdOrNull<HTMLElement>('memory-group-chips')?.addEventListener('click', (event) => {
|
|
108
|
+
const chip = (event.target as HTMLElement).closest<HTMLButtonElement>('[data-group]');
|
|
109
|
+
if (!chip?.dataset.group) return;
|
|
110
|
+
const group = chip.dataset.group;
|
|
111
|
+
if (this.groups.has(group)) this.groups.delete(group);
|
|
112
|
+
else this.groups.add(group);
|
|
113
|
+
void this.load();
|
|
114
|
+
});
|
|
115
|
+
getElementByIdOrNull<HTMLElement>('memory-timeline')?.addEventListener('click', (event) => {
|
|
116
|
+
const row = (event.target as HTMLElement).closest<HTMLElement>('[data-record-id]');
|
|
117
|
+
if (!row?.dataset.recordId) return;
|
|
118
|
+
this.select(row.dataset.recordId);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private select(id: string): void {
|
|
123
|
+
this.selectedId = id;
|
|
124
|
+
document.querySelectorAll('#memory-timeline .mt-row.selected').forEach((row) => {
|
|
125
|
+
row.classList.remove('selected');
|
|
126
|
+
});
|
|
127
|
+
document
|
|
128
|
+
.querySelector(`#memory-timeline [data-record-id="${CSS.escape(id)}"]`)
|
|
129
|
+
?.classList.add('selected');
|
|
130
|
+
this.onSelect(id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private params(): Record<string, string> {
|
|
134
|
+
return {
|
|
135
|
+
...(this.day === null ? { period: this.period } : { from: this.day, to: this.day }),
|
|
136
|
+
...(this.query === '' ? {} : { q: this.query }),
|
|
137
|
+
...(this.groups.size === 0 ? {} : { groups: [...this.groups].join(',') }),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async load(): Promise<void> {
|
|
142
|
+
const list = getElementByIdOrNull<HTMLElement>('memory-timeline');
|
|
143
|
+
if (!list) return;
|
|
144
|
+
const request = ++this.request;
|
|
145
|
+
list.setAttribute('aria-busy', 'true');
|
|
146
|
+
try {
|
|
147
|
+
const data = await API.getMemoryTimeline(this.params());
|
|
148
|
+
// A slower earlier read must not overwrite the answer to the latest filter.
|
|
149
|
+
if (request !== this.request) return;
|
|
150
|
+
this.render(data);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (request !== this.request) return;
|
|
153
|
+
list.innerHTML = `<div class="mt-empty mt-error">Could not read what was saved: ${escapeHtml(
|
|
154
|
+
error instanceof Error ? error.message : String(error)
|
|
155
|
+
)}</div>`;
|
|
156
|
+
} finally {
|
|
157
|
+
if (request === this.request) list.removeAttribute('aria-busy');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
render(data: MemoryTimelineResponse): void {
|
|
162
|
+
document.querySelectorAll<HTMLButtonElement>('[data-memory-period]').forEach((button) => {
|
|
163
|
+
button.setAttribute(
|
|
164
|
+
'aria-pressed',
|
|
165
|
+
String(this.day === null && button.dataset.memoryPeriod === this.period)
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
const range = getElementByIdOrNull<HTMLElement>('memory-timeline-range');
|
|
169
|
+
if (range) {
|
|
170
|
+
range.textContent =
|
|
171
|
+
data.from === data.to
|
|
172
|
+
? dayLabel(data.from)
|
|
173
|
+
: `${dayLabel(data.from)} – ${dayLabel(data.to)}`;
|
|
174
|
+
}
|
|
175
|
+
const count = getElementByIdOrNull<HTMLElement>('memory-timeline-count');
|
|
176
|
+
if (count) count.textContent = `${data.total} saved`;
|
|
177
|
+
this.renderChips(data.counts);
|
|
178
|
+
|
|
179
|
+
const list = getElementByIdOrNull<HTMLElement>('memory-timeline');
|
|
180
|
+
if (!list) return;
|
|
181
|
+
if (data.days.length === 0) {
|
|
182
|
+
list.innerHTML = `<div class="mt-empty">Nothing saved${
|
|
183
|
+
this.query === '' ? '' : ` matching “${escapeHtml(this.query)}”`
|
|
184
|
+
} in this period.</div>`;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
list.innerHTML = data.days
|
|
188
|
+
.map(
|
|
189
|
+
(day) =>
|
|
190
|
+
`<section class="mt-day"><h3 class="mt-day-head"><span>${escapeHtml(
|
|
191
|
+
dayLabel(day.day)
|
|
192
|
+
)}</span><span class="mt-count">${day.total}</span></h3>${day.groups
|
|
193
|
+
.map((group) => this.renderGroup(group))
|
|
194
|
+
.join('')}</section>`
|
|
195
|
+
)
|
|
196
|
+
.join('');
|
|
197
|
+
if (this.selectedId !== null) {
|
|
198
|
+
document
|
|
199
|
+
.querySelector(`#memory-timeline [data-record-id="${CSS.escape(this.selectedId)}"]`)
|
|
200
|
+
?.classList.add('selected');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private renderChips(counts: Record<string, number>): void {
|
|
205
|
+
const container = getElementByIdOrNull<HTMLElement>('memory-group-chips');
|
|
206
|
+
if (!container) return;
|
|
207
|
+
container.innerHTML = Object.entries(counts)
|
|
208
|
+
.filter(([group, count]) => count > 0 || this.groups.has(group))
|
|
209
|
+
.map(([group, count]) => {
|
|
210
|
+
const info = groupInfo(group);
|
|
211
|
+
return `<button type="button" class="memory-chip" data-group="${escapeAttr(
|
|
212
|
+
group
|
|
213
|
+
)}" aria-pressed="${this.groups.has(group)}" title="${escapeAttr(
|
|
214
|
+
info.hint
|
|
215
|
+
)}"><span class="mt-dot" style="background:${info.color}"></span>${escapeHtml(
|
|
216
|
+
info.label
|
|
217
|
+
)} <span class="mt-count">${count}</span></button>`;
|
|
218
|
+
})
|
|
219
|
+
.join('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private renderGroup(group: TimelineGroup): string {
|
|
223
|
+
const info = groupInfo(group.group);
|
|
224
|
+
const body =
|
|
225
|
+
group.items !== undefined
|
|
226
|
+
? group.items.map((item) => this.renderItem(item)).join('')
|
|
227
|
+
: (group.records ?? []).map((record) => this.renderRecord(record)).join('');
|
|
228
|
+
return `<details class="mt-group" open><summary class="mt-group-head" title="${escapeAttr(
|
|
229
|
+
info.hint
|
|
230
|
+
)}"><span class="mt-dot" style="background:${info.color}"></span>${escapeHtml(
|
|
231
|
+
info.label
|
|
232
|
+
)}<span class="mt-count">${group.count}</span></summary>${body}</details>`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private renderRecord(record: TimelineRecord): string {
|
|
236
|
+
const via = record.via === null ? '' : (VIA_LABELS[record.via] ?? record.via);
|
|
237
|
+
const status =
|
|
238
|
+
record.status === null || record.status === 'active'
|
|
239
|
+
? ''
|
|
240
|
+
: `<span class="mt-badge">${escapeHtml(record.status)}</span>`;
|
|
241
|
+
return `<button type="button" class="mt-row" data-record-id="${escapeAttr(
|
|
242
|
+
record.id
|
|
243
|
+
)}"><span class="mt-time">${escapeHtml(record.time)}</span><span class="mt-body"><span class="mt-topic">${escapeHtml(
|
|
244
|
+
record.topic
|
|
245
|
+
)}${status}</span><span class="mt-summary">${escapeHtml(record.summary)}</span>${
|
|
246
|
+
via === '' ? '' : `<span class="mt-via">from ${escapeHtml(via)}</span>`
|
|
247
|
+
}</span></button>`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private renderItem(item: TimelineItem): string {
|
|
251
|
+
const latest = item.revisions[0];
|
|
252
|
+
const updates = item.revisions.length;
|
|
253
|
+
return `<details class="mt-item"><summary class="mt-item-head"><span class="mt-time">${escapeHtml(
|
|
254
|
+
latest?.time ?? ''
|
|
255
|
+
)}</span><span class="mt-body"><span class="mt-topic">${escapeHtml(
|
|
256
|
+
item.title ?? item.topic
|
|
257
|
+
)}</span><span class="mt-summary">${escapeHtml(latest?.summary ?? '')}</span></span><span class="mt-count">${updates} ${
|
|
258
|
+
updates === 1 ? 'update' : 'updates'
|
|
259
|
+
}</span></summary>${item.revisions
|
|
260
|
+
.map(
|
|
261
|
+
(revision) =>
|
|
262
|
+
`<button type="button" class="mt-row mt-revision" data-record-id="${escapeAttr(
|
|
263
|
+
revision.id
|
|
264
|
+
)}"><span class="mt-time">${escapeHtml(revision.time)}</span><span class="mt-body"><span class="mt-op">${escapeHtml(
|
|
265
|
+
OPERATION_LABELS[revision.operation ?? ''] ?? revision.operation ?? ''
|
|
266
|
+
)}${revision.revision === null ? '' : ` · revision ${revision.revision}`}</span><span class="mt-summary">${escapeHtml(
|
|
267
|
+
revision.summary
|
|
268
|
+
)}</span></span></button>`
|
|
269
|
+
)
|
|
270
|
+
.join('')}</details>`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -79,7 +79,7 @@ declare global {
|
|
|
79
79
|
|
|
80
80
|
interface VisNetwork {
|
|
81
81
|
on(
|
|
82
|
-
event: 'click' | 'stabilized' | 'stabilizationIterationsDone',
|
|
82
|
+
event: 'click' | 'doubleClick' | 'stabilized' | 'stabilizationIterationsDone',
|
|
83
83
|
handler: (params: { nodes: Array<string | number> }) => void
|
|
84
84
|
): void;
|
|
85
85
|
on(event: 'hoverNode' | 'blurNode', handler: (params: { node: string | number }) => void): void;
|
|
@@ -50,22 +50,45 @@ export interface GraphDetailResponse {
|
|
|
50
50
|
[key: string]: unknown;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export interface
|
|
54
|
-
id
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
export interface TimelineRecord {
|
|
54
|
+
id: string;
|
|
55
|
+
kind: string | null;
|
|
56
|
+
status: string | null;
|
|
57
|
+
topic: string;
|
|
58
|
+
summary: string;
|
|
59
|
+
time: string;
|
|
60
|
+
via: string | null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export interface
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
export interface TimelineItem {
|
|
64
|
+
commitmentId: string;
|
|
65
|
+
title: string | null;
|
|
66
|
+
topic: string;
|
|
67
|
+
revisions: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
revision: number | null;
|
|
70
|
+
operation: string | null;
|
|
71
|
+
status: string | null;
|
|
72
|
+
summary: string;
|
|
73
|
+
time: string;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface TimelineGroup {
|
|
78
|
+
group: string;
|
|
79
|
+
count: number;
|
|
80
|
+
records?: TimelineRecord[];
|
|
81
|
+
items?: TimelineItem[];
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
export
|
|
84
|
+
export interface MemoryTimelineResponse {
|
|
85
|
+
from: string;
|
|
86
|
+
to: string;
|
|
87
|
+
timeZone: string;
|
|
88
|
+
total: number;
|
|
89
|
+
counts: Record<string, number>;
|
|
90
|
+
days: Array<{ day: string; total: number; groups: TimelineGroup[] }>;
|
|
91
|
+
}
|
|
69
92
|
|
|
70
93
|
export interface CheckpointSummary {
|
|
71
94
|
id?: string;
|
|
@@ -660,45 +683,18 @@ export class API {
|
|
|
660
683
|
// Graph API
|
|
661
684
|
// =============================================
|
|
662
685
|
|
|
663
|
-
/**
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
* @returns {Promise<Object>} Graph data
|
|
667
|
-
*/
|
|
668
|
-
static async getGraph(params: QueryParams = {}): Promise<GraphResponse> {
|
|
669
|
-
// cluster: false by default to avoid slow embedding calculations
|
|
670
|
-
return this.get<GraphResponse>('/graph', { cluster: 'false', ...params });
|
|
686
|
+
/** One record and the records and source messages it links to directly. */
|
|
687
|
+
static async getGraphNeighbors(id: string): Promise<GraphResponse> {
|
|
688
|
+
return this.get<GraphResponse>('/api/graph/neighbors', { id });
|
|
671
689
|
}
|
|
672
690
|
|
|
673
|
-
|
|
674
|
-
|
|
691
|
+
/** What was saved in a period, by day, kind and item. */
|
|
692
|
+
static async getMemoryTimeline(params: QueryParams): Promise<MemoryTimelineResponse> {
|
|
693
|
+
return this.get<MemoryTimelineResponse>('/api/memory/timeline', params);
|
|
675
694
|
}
|
|
676
695
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
* @param {string} nodeId - Node ID
|
|
680
|
-
* @returns {Promise<Object>} Similar decisions
|
|
681
|
-
*/
|
|
682
|
-
static async getSimilarDecisions(nodeId: string): Promise<GraphSimilarResponse> {
|
|
683
|
-
return this.get<GraphSimilarResponse>('/graph/similar', { id: nodeId });
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Update decision outcome
|
|
688
|
-
* @param {string} id - Decision ID
|
|
689
|
-
* @param {string} outcome - Outcome value
|
|
690
|
-
* @param {string} reason - Optional reason
|
|
691
|
-
* @returns {Promise<Object>} Update result
|
|
692
|
-
*/
|
|
693
|
-
static async updateOutcome(
|
|
694
|
-
id: string,
|
|
695
|
-
outcome: string,
|
|
696
|
-
reason: string | null = null
|
|
697
|
-
): Promise<JsonRecord> {
|
|
698
|
-
return this.post<JsonRecord, { id: string; outcome: string; reason: string | null }>(
|
|
699
|
-
'/graph/update',
|
|
700
|
-
{ id, outcome, reason }
|
|
701
|
-
);
|
|
696
|
+
static async getGraphDetail(nodeId: string): Promise<GraphDetailResponse> {
|
|
697
|
+
return this.get<GraphDetailResponse>('/graph/detail', { id: nodeId });
|
|
702
698
|
}
|
|
703
699
|
|
|
704
700
|
// =============================================
|
package/public/viewer/viewer.css
CHANGED
|
@@ -115,16 +115,16 @@ button:not([class*="p-"]):not([class*="px-"]):not([class*="py-"]) { padding: 6px
|
|
|
115
115
|
max-width: 100% !important;
|
|
116
116
|
border-right: none;
|
|
117
117
|
border-bottom: 1px solid #e5e7eb;
|
|
118
|
-
max-height:
|
|
118
|
+
max-height: 55vh;
|
|
119
119
|
}
|
|
120
120
|
#sidebar-resize-handle {
|
|
121
121
|
display: none;
|
|
122
122
|
}
|
|
123
123
|
#graph-container {
|
|
124
|
-
min-height:
|
|
124
|
+
min-height: 40vh;
|
|
125
125
|
}
|
|
126
126
|
#graph-canvas {
|
|
127
|
-
min-height:
|
|
127
|
+
min-height: 40vh;
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
@keyframes float {
|
|
@@ -255,14 +255,264 @@ button:not([class*="p-"]):not([class*="px-"]):not([class*="py-"]) { padding: 6px
|
|
|
255
255
|
flex-wrap: wrap;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
.memory-controls #
|
|
258
|
+
.memory-controls #memory-timeline-search {
|
|
259
259
|
min-width: 0;
|
|
260
|
-
flex:
|
|
260
|
+
flex: 1 1 12rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ============================================================================
|
|
264
|
+
MEMORY TIMELINE - period, kinds, then day > kind > item > update
|
|
265
|
+
============================================================================ */
|
|
266
|
+
|
|
267
|
+
.memory-periods,
|
|
268
|
+
.memory-groups {
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-wrap: wrap;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 4px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.memory-chip {
|
|
276
|
+
display: inline-flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 6px;
|
|
279
|
+
padding: 3px 10px;
|
|
280
|
+
border: 1px solid #e5e1d8;
|
|
281
|
+
border-radius: 9999px;
|
|
282
|
+
background: #fff;
|
|
283
|
+
color: #3f3a35;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
line-height: 18px;
|
|
286
|
+
cursor: pointer;
|
|
261
287
|
}
|
|
262
288
|
|
|
263
|
-
.memory-
|
|
289
|
+
.memory-chip:hover {
|
|
290
|
+
background: #fffaf0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.memory-chip[aria-pressed='true'] {
|
|
294
|
+
background: #131313;
|
|
295
|
+
border-color: #131313;
|
|
296
|
+
color: #fff;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.memory-chip[aria-pressed='true'] .mt-count {
|
|
300
|
+
color: #d4d0c8;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.memory-day {
|
|
304
|
+
padding: 2px 6px;
|
|
305
|
+
border: 1px solid #e5e1d8;
|
|
306
|
+
border-radius: 6px;
|
|
307
|
+
font-size: 12px;
|
|
308
|
+
color: #3f3a35;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.mt-head {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: baseline;
|
|
314
|
+
justify-content: space-between;
|
|
315
|
+
gap: 8px;
|
|
316
|
+
padding: 8px 12px;
|
|
317
|
+
border-bottom: 1px solid #ede9e1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.mt-range {
|
|
321
|
+
font-size: 13px;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
color: #1a1a1a;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.mt-total,
|
|
327
|
+
.mt-count {
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
color: #8a847c;
|
|
330
|
+
font-variant-numeric: tabular-nums;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.mt-empty {
|
|
334
|
+
padding: 24px 16px;
|
|
335
|
+
text-align: center;
|
|
336
|
+
font-size: 13px;
|
|
337
|
+
color: #8a847c;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.mt-error {
|
|
341
|
+
color: #b42318;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#memory-timeline[aria-busy='true'] {
|
|
345
|
+
opacity: 0.6;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.mt-day-head {
|
|
349
|
+
position: sticky;
|
|
350
|
+
top: 0;
|
|
351
|
+
z-index: 1;
|
|
352
|
+
display: flex;
|
|
353
|
+
justify-content: space-between;
|
|
354
|
+
padding: 8px 12px;
|
|
355
|
+
background: #f7f5f0;
|
|
356
|
+
border-bottom: 1px solid #ede9e1;
|
|
357
|
+
font-size: 13px;
|
|
358
|
+
font-weight: 600;
|
|
359
|
+
color: #1a1a1a;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.mt-group-head,
|
|
363
|
+
.mt-item-head {
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
gap: 8px;
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
list-style: none;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.mt-group-head::-webkit-details-marker,
|
|
372
|
+
.mt-item-head::-webkit-details-marker {
|
|
373
|
+
display: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.mt-group-head {
|
|
377
|
+
padding: 6px 12px;
|
|
378
|
+
font-size: 12px;
|
|
379
|
+
font-weight: 600;
|
|
380
|
+
color: #3f3a35;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.mt-group-head .mt-count {
|
|
384
|
+
margin-left: auto;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.mt-dot {
|
|
388
|
+
width: 10px;
|
|
389
|
+
height: 10px;
|
|
390
|
+
border-radius: 9999px;
|
|
391
|
+
flex-shrink: 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.mt-row,
|
|
395
|
+
.mt-item-head {
|
|
396
|
+
display: flex;
|
|
397
|
+
align-items: flex-start;
|
|
398
|
+
gap: 10px;
|
|
399
|
+
width: 100%;
|
|
400
|
+
padding: 6px 12px 6px 30px;
|
|
401
|
+
border: 0;
|
|
402
|
+
border-bottom: 1px solid #f5f2ed;
|
|
403
|
+
background: transparent;
|
|
404
|
+
text-align: left;
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.mt-row:hover,
|
|
409
|
+
.mt-item-head:hover {
|
|
410
|
+
background: #fffdf5;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.mt-row.selected {
|
|
414
|
+
background: #fff4cc;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.mt-revision {
|
|
418
|
+
padding-left: 54px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.mt-time {
|
|
422
|
+
flex-shrink: 0;
|
|
423
|
+
width: 40px;
|
|
424
|
+
font-size: 12px;
|
|
425
|
+
color: #8a847c;
|
|
426
|
+
font-variant-numeric: tabular-nums;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.mt-body {
|
|
430
|
+
display: flex;
|
|
431
|
+
flex-direction: column;
|
|
432
|
+
gap: 2px;
|
|
264
433
|
min-width: 0;
|
|
265
|
-
flex: 1
|
|
434
|
+
flex: 1;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.mt-topic,
|
|
438
|
+
.mt-op {
|
|
439
|
+
font-size: 13px;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
color: #1a1a1a;
|
|
442
|
+
overflow-wrap: anywhere;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.mt-op {
|
|
446
|
+
font-weight: 500;
|
|
447
|
+
color: #6b6560;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.mt-summary {
|
|
451
|
+
font-size: 12px;
|
|
452
|
+
line-height: 1.45;
|
|
453
|
+
color: #4a4540;
|
|
454
|
+
display: -webkit-box;
|
|
455
|
+
-webkit-line-clamp: 2;
|
|
456
|
+
-webkit-box-orient: vertical;
|
|
457
|
+
overflow: hidden;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.mt-via {
|
|
461
|
+
font-size: 11px;
|
|
462
|
+
color: #8a847c;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.mt-badge {
|
|
466
|
+
margin-left: 6px;
|
|
467
|
+
padding: 0 6px;
|
|
468
|
+
border-radius: 9999px;
|
|
469
|
+
background: #f1eee8;
|
|
470
|
+
font-size: 11px;
|
|
471
|
+
font-weight: 500;
|
|
472
|
+
color: #6b6560;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.mt-item-head .mt-count {
|
|
476
|
+
flex-shrink: 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.graph-empty-text {
|
|
480
|
+
max-width: 320px;
|
|
481
|
+
text-align: center;
|
|
482
|
+
font-size: 14px;
|
|
483
|
+
color: #8a847c;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.legend-heading {
|
|
487
|
+
margin-bottom: 6px;
|
|
488
|
+
font-size: 12px;
|
|
489
|
+
font-weight: 600;
|
|
490
|
+
color: #3f3a35;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.legend-row {
|
|
494
|
+
display: flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
gap: 8px;
|
|
497
|
+
margin-bottom: 4px;
|
|
498
|
+
font-size: 13px;
|
|
499
|
+
color: #1a1a1a;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.legend-label {
|
|
503
|
+
flex: 1;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.legend-count {
|
|
507
|
+
color: #8a847c;
|
|
508
|
+
font-variant-numeric: tabular-nums;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.legend-dot {
|
|
512
|
+
width: 12px;
|
|
513
|
+
height: 12px;
|
|
514
|
+
border-radius: 9999px;
|
|
515
|
+
flex-shrink: 0;
|
|
266
516
|
}
|
|
267
517
|
|
|
268
518
|
/* ============================================================================
|