agent-memory-git 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/PKG-INFO +7 -5
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/README.md +6 -4
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/pyproject.toml +1 -1
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/pyproject.toml.orig +1 -1
- agent_memory_git-0.1.3/src/agent_memory/webui/app.css +263 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/webui/app.js +180 -76
- agent_memory_git-0.1.3/src/agent_memory/webui/index.html +47 -0
- agent_memory_git-0.1.2/src/agent_memory/webui/app.css +0 -192
- agent_memory_git-0.1.2/src/agent_memory/webui/index.html +0 -31
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/LICENSE +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/PROTOCOL.md +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/__init__.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/__main__.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/capture.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/cli.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/config.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/context.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/embed_worker.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/frontmatter.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/gitsync.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/hooks.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/importers.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/install.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/lint.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/select.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/sessions.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/store.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/ui.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/ui_server.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/util.py +0 -0
- {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/workdir.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-memory-git
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Shared, git-backed memory for Claude Code and Codex.
|
|
5
5
|
Keywords: claude-code,codex,memory,agents,git
|
|
6
6
|
Author: Shichao Song
|
|
@@ -131,11 +131,13 @@ terminal open while you use it. It needs `numpy`; the hooks do not.
|
|
|
131
131
|
- **Find.** One search box (press `/`). Word matches appear as you type, with the matching words
|
|
132
132
|
marked; a moment later the local model adds memories close in meaning, across Chinese and
|
|
133
133
|
English. Filters narrow the list by type, where a memory loads (everywhere, or one project or
|
|
134
|
-
folder), which tool wrote it, keyword,
|
|
135
|
-
|
|
134
|
+
folder), which tool wrote it, keyword, when it was added (a recent period or a date range), and
|
|
135
|
+
status (needs tidying, as `mem lint` reports and the open memory lists, or unchanged for 60
|
|
136
|
+
days). Tick several values in one filter to see memories that match any of them. The date
|
|
137
|
+
column's header sets the order: newest, oldest, recently changed or A to Z.
|
|
136
138
|
- **Read.** Click a row, or move with `j`/`k` and press Enter. The memory opens on the right with
|
|
137
|
-
|
|
138
|
-
`
|
|
139
|
+
the body rendered as Markdown with `[[links]]` that open other memories, every frontmatter field
|
|
140
|
+
(including each `source` entry), the most similar memories, and the file exactly as stored.
|
|
139
141
|
- **Change.** Edit (`e`) or delete from the open memory, add one with New (`n`), or tick several
|
|
140
142
|
rows (`x`) and delete them together. Every change is one git commit, pushed in the background,
|
|
141
143
|
with Undo. If an agent edits the same file meanwhile, saving is refused rather than overwriting.
|
|
@@ -111,11 +111,13 @@ terminal open while you use it. It needs `numpy`; the hooks do not.
|
|
|
111
111
|
- **Find.** One search box (press `/`). Word matches appear as you type, with the matching words
|
|
112
112
|
marked; a moment later the local model adds memories close in meaning, across Chinese and
|
|
113
113
|
English. Filters narrow the list by type, where a memory loads (everywhere, or one project or
|
|
114
|
-
folder), which tool wrote it, keyword,
|
|
115
|
-
|
|
114
|
+
folder), which tool wrote it, keyword, when it was added (a recent period or a date range), and
|
|
115
|
+
status (needs tidying, as `mem lint` reports and the open memory lists, or unchanged for 60
|
|
116
|
+
days). Tick several values in one filter to see memories that match any of them. The date
|
|
117
|
+
column's header sets the order: newest, oldest, recently changed or A to Z.
|
|
116
118
|
- **Read.** Click a row, or move with `j`/`k` and press Enter. The memory opens on the right with
|
|
117
|
-
|
|
118
|
-
`
|
|
119
|
+
the body rendered as Markdown with `[[links]]` that open other memories, every frontmatter field
|
|
120
|
+
(including each `source` entry), the most similar memories, and the file exactly as stored.
|
|
119
121
|
- **Change.** Edit (`e`) or delete from the open memory, add one with New (`n`), or tick several
|
|
120
122
|
rows (`x`) and delete them together. Every change is one git commit, pushed in the background,
|
|
121
123
|
with Undo. If an agent edits the same file meanwhile, saving is refused rather than overwriting.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/* Memories: a header, then search, filters and column names over the list, and one memory at a time in the drawer.
|
|
2
|
+
The header, toolbar and list share one column (.wrap), and the column names and rows share one grid (--cols),
|
|
3
|
+
so everything lines up on the same left edge. */
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: light;
|
|
6
|
+
--page: #f7f7f5;
|
|
7
|
+
--surface: #ffffff;
|
|
8
|
+
--ink: #171716;
|
|
9
|
+
--ink-2: #57564f;
|
|
10
|
+
--ink-3: #8d8c85;
|
|
11
|
+
--line: #e6e5df;
|
|
12
|
+
--line-soft: #efeee9;
|
|
13
|
+
--d1: #efeee9;
|
|
14
|
+
--accent: #2a78d6;
|
|
15
|
+
--accent-soft: rgba(42, 120, 214, 0.1);
|
|
16
|
+
--danger: #c93838;
|
|
17
|
+
--warn: #b26a12;
|
|
18
|
+
--mark: #ba7517;
|
|
19
|
+
--shadow: 0 12px 40px rgba(20, 20, 18, 0.14);
|
|
20
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
21
|
+
--t-user: #8a62cf; --t-feedback: #2a78d6; --t-project: #3c9a6b; --t-reference: #df8530;
|
|
22
|
+
--drawer: min(600px, 100vw);
|
|
23
|
+
--cols: 16px minmax(0, 1fr) 92px 200px 96px;
|
|
24
|
+
}
|
|
25
|
+
@media (prefers-color-scheme: dark) {
|
|
26
|
+
:root {
|
|
27
|
+
color-scheme: dark;
|
|
28
|
+
--page: #121211;
|
|
29
|
+
--surface: #1b1b1a;
|
|
30
|
+
--ink: #f2f2ef;
|
|
31
|
+
--ink-2: #bcbbb3;
|
|
32
|
+
--ink-3: #85847d;
|
|
33
|
+
--line: #2b2b29;
|
|
34
|
+
--line-soft: #222220;
|
|
35
|
+
--d1: #242422;
|
|
36
|
+
--accent: #4b93ea;
|
|
37
|
+
--accent-soft: rgba(75, 147, 234, 0.16);
|
|
38
|
+
--danger: #e66767;
|
|
39
|
+
--warn: #e2a24f;
|
|
40
|
+
--shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
|
|
41
|
+
--t-user: #a98ae6; --t-feedback: #6aa1ec; --t-project: #5fbf8e; --t-reference: #f0a45c;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
* { box-sizing: border-box; }
|
|
46
|
+
[hidden] { display: none !important; }
|
|
47
|
+
html, body { margin: 0; }
|
|
48
|
+
body { background: var(--page); color: var(--ink); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", "PingFang SC", sans-serif; }
|
|
49
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
50
|
+
button { cursor: pointer; }
|
|
51
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
52
|
+
|
|
53
|
+
.btn { height: 32px; border: 1px solid var(--line); background: var(--surface); border-radius: 8px; padding: 0 12px; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
54
|
+
.btn:hover { background: var(--d1); }
|
|
55
|
+
.btn.primary { background: var(--ink); color: var(--surface); border-color: var(--ink); font-weight: 600; }
|
|
56
|
+
.btn.primary:hover { opacity: .9; background: var(--ink); }
|
|
57
|
+
.btn.danger { color: var(--danger); }
|
|
58
|
+
.btn:disabled { opacity: .45; cursor: default; }
|
|
59
|
+
.icon-btn { border: 0; background: none; width: 32px; height: 32px; border-radius: 8px; display: inline-grid; place-items: center; color: var(--ink-2); flex: none; }
|
|
60
|
+
.icon-btn:hover { background: var(--d1); color: var(--ink); }
|
|
61
|
+
.icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
62
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); flex: none; }
|
|
63
|
+
.dot.user { background: var(--t-user); } .dot.feedback { background: var(--t-feedback); }
|
|
64
|
+
.dot.project { background: var(--t-project); } .dot.reference { background: var(--t-reference); }
|
|
65
|
+
|
|
66
|
+
/* ---------- page and header */
|
|
67
|
+
.page { container: page / inline-size; }
|
|
68
|
+
.page { --pad: 32px; }
|
|
69
|
+
.wrap { max-width: 1120px; margin: 0 auto; padding: 0 var(--pad); }
|
|
70
|
+
.head { background: var(--surface); border-bottom: 1px solid var(--line); }
|
|
71
|
+
.head .wrap { display: flex; align-items: center; gap: 20px; height: 56px; }
|
|
72
|
+
.brand { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
|
|
73
|
+
.mark { width: 24px; height: 24px; flex: none; }
|
|
74
|
+
.mark rect { fill: var(--mark); }
|
|
75
|
+
.mark path { fill: none; stroke: #fff; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
76
|
+
.brand .name { font-size: 15px; font-weight: 650; letter-spacing: -.01em; white-space: nowrap; }
|
|
77
|
+
.brand .repo { min-width: 0; font: 12px var(--mono); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
78
|
+
.brand .repo:not(:empty)::before { content: "/"; margin-right: 10px; font-family: system-ui, sans-serif; color: var(--line); }
|
|
79
|
+
.status { display: flex; align-items: center; gap: 16px; font-size: 12.5px; color: var(--ink-3); white-space: nowrap; }
|
|
80
|
+
.status span { display: inline-flex; align-items: center; gap: 6px; }
|
|
81
|
+
.status .warn { color: var(--warn); }
|
|
82
|
+
.status .warn .dot { background: var(--warn); }
|
|
83
|
+
|
|
84
|
+
/* ---------- toolbar: search, filters, column names */
|
|
85
|
+
.top { position: sticky; top: 0; z-index: 10; background: color-mix(in srgb, var(--page) 94%, transparent); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); }
|
|
86
|
+
.top .wrap { padding-top: 16px; }
|
|
87
|
+
.search { position: relative; }
|
|
88
|
+
.search input { width: 100%; height: 40px; padding: 0 44px; border-radius: 10px; border: 1px solid var(--line); background: var(--surface); outline: none; font-size: 15px; }
|
|
89
|
+
.search input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
90
|
+
.search input::-webkit-search-cancel-button { display: none; }
|
|
91
|
+
.search-ico { position: absolute; left: 16px; top: 12px; width: 16px; height: 16px; fill: none; stroke: var(--ink-3); stroke-width: 1.6; stroke-linecap: round; }
|
|
92
|
+
.search kbd { position: absolute; right: 12px; top: 10px; font: 11px/18px var(--mono); color: var(--ink-3); border: 1px solid var(--line); border-radius: 5px; padding: 0 6px; }
|
|
93
|
+
|
|
94
|
+
/* filters: one quiet menu per filter, text until it is on */
|
|
95
|
+
.filters { padding: 10px 0 12px; }
|
|
96
|
+
.frow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-height: 32px; }
|
|
97
|
+
.filters .clear { height: 28px; border: 0; background: none; color: var(--accent); font-size: 13px; padding: 0 8px; border-radius: 6px; white-space: nowrap; }
|
|
98
|
+
.filters .clear:hover { background: var(--accent-soft); }
|
|
99
|
+
.fm { position: relative; display: inline-flex; align-items: center; height: 32px; border-radius: 8px; }
|
|
100
|
+
.fm:hover, .fm.open { background: var(--d1); }
|
|
101
|
+
.fm.on { background: var(--accent-soft); }
|
|
102
|
+
.q { position: relative; display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 24px 0 10px; border: 0; background: none; border-radius: 8px; font-size: 13px; color: var(--ink-3); white-space: nowrap; }
|
|
103
|
+
.q .qv { color: var(--ink); max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
|
|
104
|
+
.q::after { content: ""; position: absolute; right: 10px; top: 50%; width: 5px; height: 5px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: translateY(-70%) rotate(45deg); pointer-events: none; }
|
|
105
|
+
.q select { position: absolute; inset: 0; width: 100%; opacity: 0; cursor: pointer; }
|
|
106
|
+
.fm.on .q { color: var(--accent); padding-right: 2px; }
|
|
107
|
+
.fm.on .q .qv { color: var(--accent); font-weight: 600; }
|
|
108
|
+
.fm.on .q::after { display: none; }
|
|
109
|
+
.qx { border: 0; background: none; color: var(--accent); width: 22px; height: 22px; margin-right: 4px; border-radius: 5px; font-size: 15px; line-height: 1; flex: none; }
|
|
110
|
+
.qx:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
|
|
111
|
+
.menu { position: absolute; left: 0; top: calc(100% + 4px); z-index: 15; width: 280px; max-width: calc(100vw - 32px); padding: 6px; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); }
|
|
112
|
+
.menu.right { left: auto; right: 0; }
|
|
113
|
+
.mfind { width: 100%; height: 32px; margin-bottom: 4px; padding: 0 10px; border: 1px solid var(--line); border-radius: 7px; background: var(--page); outline: none; font-size: 13px; }
|
|
114
|
+
.mfind:focus { border-color: var(--accent); }
|
|
115
|
+
.mlist { max-height: 300px; overflow-y: auto; }
|
|
116
|
+
.mi { display: flex; align-items: center; gap: 10px; width: 100%; height: 32px; padding: 0 8px; border: 0; background: none; border-radius: 6px; font-size: 13px; color: var(--ink); text-align: left; }
|
|
117
|
+
.mi:hover, .mi:focus-visible { background: var(--d1); outline: none; }
|
|
118
|
+
.mi.zero { color: var(--ink-3); }
|
|
119
|
+
.mi .mt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
120
|
+
.mi .n { font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
121
|
+
.mi .box { width: 15px; height: 15px; flex: none; border: 1.5px solid var(--ink-3); border-radius: 4px; display: grid; place-items: center; }
|
|
122
|
+
.mi .box.radio { border-radius: 50%; }
|
|
123
|
+
.mi.on .box { border-color: var(--accent); background: var(--accent); }
|
|
124
|
+
.mi.on .box::after { content: ""; width: 4px; height: 8px; margin-top: -2px; border: solid #fff; border-width: 0 1.8px 1.8px 0; transform: rotate(45deg); }
|
|
125
|
+
.mi.on .box.radio::after { width: 5px; height: 5px; margin: 0; border: 0; border-radius: 50%; background: #fff; transform: none; }
|
|
126
|
+
.mnone { padding: 8px; font-size: 13px; color: var(--ink-3); }
|
|
127
|
+
.mrange { margin-top: 6px; padding: 8px 8px 4px; border-top: 1px solid var(--line); }
|
|
128
|
+
.mrange .mh { font-size: 12px; color: var(--ink-3); margin-bottom: 6px; }
|
|
129
|
+
.mrange .dates { display: flex; align-items: center; gap: 6px; color: var(--ink-3); }
|
|
130
|
+
.mrange input { flex: 1; min-width: 0; height: 32px; padding: 0 6px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface); font-size: 12.5px; color: var(--ink); }
|
|
131
|
+
.mfoot { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; }
|
|
132
|
+
|
|
133
|
+
.cols, .row { display: grid; grid-template-columns: var(--cols); column-gap: 12px; padding-left: 16px; padding-right: 16px; }
|
|
134
|
+
.cols { height: 34px; align-items: center; border-top: 1px solid var(--line); font-size: 12px; font-weight: 500; color: var(--ink-3); }
|
|
135
|
+
.c-date { text-align: right; }
|
|
136
|
+
.cols .c-count { color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
137
|
+
.cols .c-date { display: flex; justify-content: flex-end; }
|
|
138
|
+
.cols .fm.sort { height: 26px; margin-right: -8px; }
|
|
139
|
+
.cols .fm.sort .q { height: 26px; padding: 0 8px; font-size: 12px; }
|
|
140
|
+
.cols .fm.sort .qv { color: var(--ink-2); font-weight: 500; }
|
|
141
|
+
.cols .fm.sort .q::after { display: none; }
|
|
142
|
+
/* with a memory open beside the list, its place is in the drawer: give the column to the descriptions */
|
|
143
|
+
body.drawer-open .page { --cols: 16px minmax(0, 1fr) 92px 96px; }
|
|
144
|
+
body.drawer-open .c-place { display: none; }
|
|
145
|
+
|
|
146
|
+
/* ---------- the list */
|
|
147
|
+
.list { padding-bottom: 120px; }
|
|
148
|
+
.list .group { font-size: 12px; font-weight: 600; color: var(--ink-3); padding: 20px 16px 8px 44px; letter-spacing: .02em; border-bottom: 1px solid var(--line-soft); }
|
|
149
|
+
.row { align-items: start; padding-top: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--line-soft); cursor: pointer; }
|
|
150
|
+
.row:hover { background: var(--surface); }
|
|
151
|
+
.row.picked { background: var(--d1); }
|
|
152
|
+
.row.picked .kw { background: var(--surface); }
|
|
153
|
+
.row.cursor { box-shadow: inset 3px 0 0 var(--ink-3); }
|
|
154
|
+
.row.open { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
|
|
155
|
+
.row .chk { width: 16px; height: 16px; margin: 2.5px 0 0; opacity: 0; cursor: pointer; }
|
|
156
|
+
.row:hover .chk, .row.picked .chk, .selecting .row .chk, .row.cursor .chk { opacity: 1; }
|
|
157
|
+
.row .main { min-width: 0; }
|
|
158
|
+
.row .desc { font-weight: 500; line-height: 21px; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
159
|
+
.row .snip { font-size: 12.5px; color: var(--ink-3); margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
160
|
+
.row mark { background: color-mix(in srgb, var(--accent) 22%, transparent); color: inherit; border-radius: 3px; padding: 0 1px; }
|
|
161
|
+
.row .c-type, .row .c-place, .row .c-date { font-size: 12.5px; line-height: 21px; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
162
|
+
.row .c-type { display: flex; align-items: center; gap: 7px; }
|
|
163
|
+
.row .c-date { font-variant-numeric: tabular-nums; }
|
|
164
|
+
.kws { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
|
|
165
|
+
.kw { border: 0; background: var(--d1); color: var(--ink-2); font-size: 11.5px; line-height: 20px; border-radius: 6px; padding: 0 7px; }
|
|
166
|
+
.kw:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--ink); }
|
|
167
|
+
.kw.on { background: var(--ink); color: var(--surface); }
|
|
168
|
+
.empty { text-align: center; color: var(--ink-3); padding: 72px 0; }
|
|
169
|
+
.empty .btn { margin-top: 12px; }
|
|
170
|
+
|
|
171
|
+
.selbar { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); display: flex; gap: 8px; align-items: center; background: var(--ink); color: var(--surface); padding: 8px 8px 8px 16px; border-radius: 12px; box-shadow: var(--shadow); z-index: 20; }
|
|
172
|
+
.selbar .n { font-weight: 600; margin-right: 6px; }
|
|
173
|
+
.selbar .btn { background: transparent; color: var(--surface); border-color: color-mix(in srgb, var(--surface) 30%, transparent); }
|
|
174
|
+
.selbar .btn:hover { background: color-mix(in srgb, var(--surface) 14%, transparent); }
|
|
175
|
+
.selbar .btn.danger { background: var(--danger); border-color: var(--danger); color: #fff; }
|
|
176
|
+
|
|
177
|
+
/* narrower list (small window, or the drawer open beside it): drop columns before squeezing the description */
|
|
178
|
+
@container page (max-width: 900px) {
|
|
179
|
+
.wrap { --cols: 16px minmax(0, 1fr) 92px 96px; }
|
|
180
|
+
.cols .c-place, .row .c-place { display: none; }
|
|
181
|
+
}
|
|
182
|
+
@container page (max-width: 640px) {
|
|
183
|
+
.wrap { --pad: 16px; }
|
|
184
|
+
.wrap { --cols: 16px minmax(0, 1fr) 88px; }
|
|
185
|
+
.cols .c-type, .row .c-type, .brand .repo, .status, .search kbd { display: none; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* with the drawer open on a wide window, the page moves over to stay readable */
|
|
189
|
+
@media (min-width: 1300px) {
|
|
190
|
+
body.drawer-open .page { margin-right: var(--drawer); }
|
|
191
|
+
body.drawer-open .selbar { left: calc((100vw - var(--drawer)) / 2); }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ---------- drawer: one memory, everything in its file */
|
|
195
|
+
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: var(--drawer); background: var(--surface); border-left: 1px solid var(--line); box-shadow: var(--shadow); transform: translateX(104%); transition: transform .22s ease; z-index: 30; display: flex; flex-direction: column; }
|
|
196
|
+
.drawer.open { transform: none; }
|
|
197
|
+
.d-head { padding: 12px 24px 0; border-bottom: 1px solid var(--line); }
|
|
198
|
+
.d-bar { display: flex; align-items: center; gap: 2px; height: 32px; margin-right: -8px; }
|
|
199
|
+
.d-bar .where { flex: 1; min-width: 0; margin-right: 8px; font: 12px var(--mono); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
200
|
+
.d-head h1 { font-size: 19px; line-height: 1.4; margin: 10px 0 0; font-weight: 650; overflow-wrap: anywhere; }
|
|
201
|
+
.d-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-top: 8px; font-size: 13px; color: var(--ink-2); }
|
|
202
|
+
.d-meta > span { display: inline-flex; align-items: center; gap: 7px; overflow-wrap: anywhere; }
|
|
203
|
+
.d-meta .sep { color: var(--ink-3); }
|
|
204
|
+
.d-head .kws { margin-top: 12px; }
|
|
205
|
+
.issues { margin: 12px 0 0; padding: 8px 12px 8px 28px; border-radius: 8px; background: color-mix(in srgb, #d9822b 10%, transparent); color: var(--ink-2); font-size: 12.5px; }
|
|
206
|
+
.issues li { margin: 1px 0; }
|
|
207
|
+
.tabs { display: flex; gap: 20px; margin-top: 12px; }
|
|
208
|
+
.tabs button { border: 0; border-bottom: 2px solid transparent; background: none; padding: 8px 0; margin-bottom: -1px; font-size: 13px; color: var(--ink-3); }
|
|
209
|
+
.tabs button:hover { color: var(--ink); }
|
|
210
|
+
.tabs button[aria-selected="true"] { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }
|
|
211
|
+
.d-body { flex: 1; overflow-y: auto; padding: 20px 24px 48px; }
|
|
212
|
+
.section { margin-top: 32px; }
|
|
213
|
+
.section h2 { font-size: 12px; font-weight: 600; color: var(--ink-3); margin: 0 0 8px; letter-spacing: .02em; }
|
|
214
|
+
.props { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 20px; margin: 0; padding: 12px 16px; background: var(--page); border-radius: 8px; font-size: 12.5px; }
|
|
215
|
+
.props dt { color: var(--ink-3); white-space: nowrap; }
|
|
216
|
+
.props dd { margin: 0; color: var(--ink-2); overflow-wrap: anywhere; font: 12px/1.6 var(--mono); }
|
|
217
|
+
.props .sect { grid-column: 1 / -1; margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 12px; font-weight: 600; color: var(--ink-2); }
|
|
218
|
+
pre.raw { white-space: pre-wrap; overflow-wrap: anywhere; font: 12.5px/1.6 var(--mono); background: var(--page); border-radius: 8px; padding: 14px 16px; margin: 0; }
|
|
219
|
+
.similar button { display: flex; gap: 12px; width: calc(100% + 16px); margin: 0 -8px; text-align: left; border: 0; background: none; padding: 7px 8px; border-radius: 8px; align-items: baseline; }
|
|
220
|
+
.similar button:hover { background: var(--d1); }
|
|
221
|
+
.similar .t { flex: 1; overflow-wrap: anywhere; }
|
|
222
|
+
.similar .p { font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
223
|
+
|
|
224
|
+
/* rendered markdown */
|
|
225
|
+
.md { font-size: 14.5px; line-height: 1.65; overflow-wrap: anywhere; }
|
|
226
|
+
.md > :first-child { margin-top: 0; }
|
|
227
|
+
.md > :last-child { margin-bottom: 0; }
|
|
228
|
+
.md h1, .md h2, .md h3, .md h4 { line-height: 1.35; margin: 1.3em 0 .5em; }
|
|
229
|
+
.md h1 { font-size: 17px; } .md h2 { font-size: 15.5px; } .md h3, .md h4 { font-size: 14.5px; }
|
|
230
|
+
.md p { margin: .6em 0; }
|
|
231
|
+
.md ul, .md ol { padding-left: 1.4em; margin: .5em 0; }
|
|
232
|
+
.md li { margin: .2em 0; }
|
|
233
|
+
.md code { font: 12.5px var(--mono); background: var(--d1); padding: 1px 5px; border-radius: 5px; }
|
|
234
|
+
.md pre { background: var(--page); border-radius: 8px; padding: 12px 16px; overflow-x: auto; }
|
|
235
|
+
.md pre code { background: none; padding: 0; white-space: pre; }
|
|
236
|
+
.md blockquote { margin: .6em 0; padding: 2px 14px; border-left: 3px solid var(--line); color: var(--ink-2); }
|
|
237
|
+
.md table { border-collapse: collapse; font-size: 13px; margin: .8em 0; display: block; overflow-x: auto; }
|
|
238
|
+
.md th, .md td { border: 1px solid var(--line); padding: 5px 9px; text-align: left; vertical-align: top; }
|
|
239
|
+
.md hr { border: 0; border-top: 1px solid var(--line); margin: 1.2em 0; }
|
|
240
|
+
.md a { color: var(--accent); }
|
|
241
|
+
.md .wikilink { color: var(--accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
|
242
|
+
|
|
243
|
+
/* editor */
|
|
244
|
+
.form { display: grid; gap: 16px; }
|
|
245
|
+
.form label { display: grid; gap: 6px; font-size: 12px; color: var(--ink-3); font-weight: 600; }
|
|
246
|
+
.form input[type="text"], .form textarea, .form select { border: 1px solid var(--line); background: var(--surface); border-radius: 8px; padding: 8px 11px; font-weight: 400; color: var(--ink); font-size: 14px; }
|
|
247
|
+
.form select { height: 36px; padding-top: 0; padding-bottom: 0; }
|
|
248
|
+
.form textarea.desc { font-size: 15px; font-weight: 500; resize: vertical; min-height: 48px; }
|
|
249
|
+
.form textarea.body { font: 13px/1.6 var(--mono); min-height: 320px; resize: vertical; }
|
|
250
|
+
.form .pair { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
|
251
|
+
.seg { display: inline-flex; height: 36px; border: 1px solid var(--line); border-radius: 8px; padding: 2px; gap: 2px; }
|
|
252
|
+
.seg button { border: 0; background: none; padding: 0 12px; border-radius: 6px; font-size: 13px; color: var(--ink-2); font-weight: 400; }
|
|
253
|
+
.seg button[aria-pressed="true"] { background: var(--d1); color: var(--ink); font-weight: 600; }
|
|
254
|
+
.form .hint { font-weight: 400; color: var(--ink-3); }
|
|
255
|
+
.form .hint.over { color: var(--danger); }
|
|
256
|
+
.d-foot { padding: 12px 24px; border-top: 1px solid var(--line); display: flex; gap: 8px; justify-content: flex-end; align-items: center; }
|
|
257
|
+
.d-foot .err { flex: 1; color: var(--danger); font-size: 13px; }
|
|
258
|
+
|
|
259
|
+
.toasts { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); display: grid; gap: 8px; z-index: 40; }
|
|
260
|
+
.toast { background: var(--ink); color: var(--surface); border-radius: 10px; padding: 10px 14px; font-size: 13px; display: flex; align-items: center; gap: 16px; box-shadow: var(--shadow); }
|
|
261
|
+
.toast.error { background: var(--danger); color: #fff; }
|
|
262
|
+
.toast button { border: 0; background: none; color: inherit; font-weight: 700; padding: 0; text-decoration: underline; text-underline-offset: 2px; }
|
|
263
|
+
.boot { position: fixed; inset: 0; display: grid; place-items: center; background: var(--page); color: var(--ink-3); z-index: 50; text-align: center; padding: 24px; }
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
const S = {
|
|
68
68
|
open: null, mode: "view", tab: "rendered",
|
|
69
69
|
q: "", lex: [], sem: [],
|
|
70
|
-
f: { type:
|
|
70
|
+
f: { type: [], loads: [], tool: [], kw: [], added: "", status: [] }, sort: local.get("sort", "new"), countText: "", menu: null, menuFind: "",
|
|
71
71
|
picked: new Set(), rows: [], cursor: -1,
|
|
72
72
|
};
|
|
73
73
|
|
|
@@ -76,13 +76,13 @@
|
|
|
76
76
|
D = d; M = d.memories;
|
|
77
77
|
byId = new Map(M.map((m, i) => [m.id, i]));
|
|
78
78
|
byName = new Map(M.map((m, i) => [m.meta && m.meta.name, i]));
|
|
79
|
-
M.forEach(m => { m.termSet = new Set(m.terms || []); });
|
|
79
|
+
M.forEach(m => { m.termSet = new Set(m.terms || []); m.day = localDay(m.created); });
|
|
80
80
|
kwName = new Map();
|
|
81
81
|
M.forEach(m => (m.keywords || []).forEach(k => kwName.set(k.k, k.t)));
|
|
82
82
|
S.picked.forEach(id => { if (!byId.has(id)) S.picked.delete(id); });
|
|
83
83
|
if (S.open && !byId.has(S.open)) closeDrawer(true);
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
S.f.loads = S.f.loads.filter(v => v === "global" || (D.places || []).some(p => "place:" + p.key === v));
|
|
85
|
+
renderStatus();
|
|
86
86
|
if (S.q) runSearch(S.q, false);
|
|
87
87
|
renderFilters();
|
|
88
88
|
renderList();
|
|
@@ -92,68 +92,165 @@
|
|
|
92
92
|
// ------------------------------------------------------------ filters
|
|
93
93
|
const TYPES = ["user", "feedback", "project", "reference"];
|
|
94
94
|
const SORTS = [["new", "Newest first"], ["old", "Oldest first"], ["edited", "Recently changed"], ["az", "A to Z"]];
|
|
95
|
-
const
|
|
95
|
+
const ADDED = [["today", "Today"], ["7", "Past 7 days"], ["30", "Past 30 days"], ["90", "Past 3 months"], ["365", "Past year"]];
|
|
96
|
+
const STATUS = [["tidy", "needs tidying"], ["stale", "unchanged 60+ days"]];
|
|
97
|
+
const DAY = 864e5;
|
|
96
98
|
const touched = m => m.updated || m.created || "";
|
|
99
|
+
const ymd = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
100
|
+
const localDay = iso => { const t = Date.parse(iso || ""); return t ? ymd(new Date(t)) : ""; };
|
|
101
|
+
const shortDay = s => new Date(`${s}T00:00`).toLocaleDateString(undefined,
|
|
102
|
+
{ month: "short", day: "numeric", year: s.slice(0, 4) === String(new Date().getFullYear()) ? undefined : "numeric" });
|
|
97
103
|
|
|
104
|
+
// Added holds one choice: a preset ("today", or the number of days back) or "range:<from>:<to>" with either end open
|
|
105
|
+
function addedOk(m, added) {
|
|
106
|
+
if (!added) return true;
|
|
107
|
+
if (!m.day) return false;
|
|
108
|
+
if (added === "today") return m.day === ymd(new Date());
|
|
109
|
+
if (added.startsWith("range:")) { const [, from, to] = added.split(":"); return (!from || m.day >= from) && (!to || m.day <= to); }
|
|
110
|
+
return Date.now() - Date.parse(m.created) <= Number(added) * DAY;
|
|
111
|
+
}
|
|
112
|
+
// the other filters hold any number of values; a memory needs to match one of them
|
|
113
|
+
const TEST = {
|
|
114
|
+
type: (m, v) => m.type === v,
|
|
115
|
+
loads: (m, v) => v === "global" ? m.scope === "global" : m.scope === "here" && m.place === v.slice(6),
|
|
116
|
+
tool: (m, v) => m.tool === v,
|
|
117
|
+
kw: (m, v) => m.termSet.has(v),
|
|
118
|
+
status: (m, v) => v === "tidy" ? (m.issues || []).length > 0 : Date.now() - (Date.parse(touched(m)) || Date.now()) >= 60 * DAY,
|
|
119
|
+
};
|
|
120
|
+
const noFilters = () => ({ type: [], loads: [], tool: [], kw: [], added: "", status: [] });
|
|
121
|
+
const filtering = () => Object.values(S.f).filter(v => v.length).length;
|
|
122
|
+
// skip leaves one filter out, so its menu can count what each of its options would show
|
|
98
123
|
function passes(m, skip) {
|
|
99
|
-
const f
|
|
100
|
-
|
|
101
|
-
if (skip !== "loads" && f.loads === "global" && m.scope !== "global") return false;
|
|
102
|
-
if (skip !== "loads" && f.loads.startsWith("place:") && (m.scope !== "here" || m.place !== f.loads.slice(6))) return false;
|
|
103
|
-
if (skip !== "tool" && f.tool && m.tool !== f.tool) return false;
|
|
104
|
-
if (skip !== "kw" && f.kw && !m.termSet.has(f.kw)) return false;
|
|
105
|
-
if (skip !== "fmt" && f.fmt && !(m.issues || []).length) return false;
|
|
106
|
-
if (skip !== "age" && f.age) {
|
|
107
|
-
const t = Date.parse(f.age === "stale" ? touched(m) : m.created || "");
|
|
108
|
-
if (!t) return false;
|
|
109
|
-
if (f.age === "stale" ? now - t < 60 * 864e5 : now - t > Number(f.age) * 864e5) return false;
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
124
|
+
for (const [key, test] of Object.entries(TEST)) if (key !== skip && S.f[key].length && !S.f[key].some(v => test(m, v))) return false;
|
|
125
|
+
return skip === "added" || addedOk(m, S.f.added);
|
|
112
126
|
}
|
|
113
|
-
|
|
114
|
-
const sel = el("select", { "aria-label": label, onchange: e => onchange(e.target.value) },
|
|
115
|
-
options.map(([v, text]) => el("option", { value: v, text, selected: v === value })));
|
|
116
|
-
return el("span", { class: "pill" + (value && key !== "sort" ? " on" : "") }, sel);
|
|
117
|
-
}
|
|
118
|
-
// keywords that tag at least two memories, most common first; the chosen one always listed
|
|
127
|
+
// keywords that tag at least two memories, most common first; chosen ones always listed
|
|
119
128
|
function keywordOptions() {
|
|
120
129
|
const n = new Map();
|
|
121
130
|
M.forEach(m => { if (passes(m, "kw")) m.termSet.forEach(t => { if (kwName.has(t)) n.set(t, (n.get(t) || 0) + 1); }); });
|
|
122
131
|
const out = [...n].filter(([, c]) => c >= 2).sort((a, b) => b[1] - a[1] || kwName.get(a[0]).localeCompare(kwName.get(b[0]))).slice(0, 80);
|
|
123
|
-
|
|
124
|
-
return out.map(([k, c]) => [k,
|
|
132
|
+
for (const k of S.f.kw) if (!out.some(([x]) => x === k)) out.unshift([k, n.get(k) || 0]);
|
|
133
|
+
return out.map(([k, c]) => [k, kwName.get(k) || k, c]);
|
|
125
134
|
}
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
// every filter as data, options as [value, label, count]
|
|
136
|
+
function filterDefs() {
|
|
137
|
+
const opts = (key, list) => list.map(([v, t]) => [v, t, M.filter(m => passes(m, key) && TEST[key](m, v)).length]);
|
|
138
|
+
const tools = [...new Set(M.map(m => m.tool).filter(Boolean))].sort();
|
|
139
|
+
return [
|
|
140
|
+
{ key: "type", label: "Type", any: "All", options: opts("type", TYPES.map(t => [t, t])) },
|
|
141
|
+
{ key: "loads", label: "Loads in", any: "Anywhere", options: opts("loads", [["global", "everywhere"], ...(D.places || []).map(p => ["place:" + p.key, p.key])]) },
|
|
142
|
+
tools.length > 1 ? { key: "tool", label: "Source", any: "Any", options: opts("tool", tools.map(t => [t, t])) } : null,
|
|
143
|
+
{ key: "kw", label: "Keyword", any: "Any", options: keywordOptions(), find: true },
|
|
144
|
+
{ key: "added", label: "Added", any: "Any time", single: true, options: ADDED.map(([v, t]) => [v, t, M.filter(m => passes(m, "added") && addedOk(m, v)).length]) },
|
|
145
|
+
{ key: "status", label: "Status", any: "Any", options: opts("status", STATUS) },
|
|
146
|
+
].filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
// the button keeps short: a place shows as its last folder (the menu and the tooltip show it whole)
|
|
149
|
+
function shownAs(d, whole) {
|
|
150
|
+
const v = S.f[d.key], full = x => (d.options.find(([o]) => o === x) || [x, x])[1];
|
|
151
|
+
const name = x => !whole && x.startsWith("place:") ? x.split("/").filter(Boolean).pop() : full(x);
|
|
152
|
+
if (!v.length) return d.any;
|
|
153
|
+
if (d.single) {
|
|
154
|
+
if (!v.startsWith("range:")) return name(v);
|
|
155
|
+
const [, from, to] = v.split(":");
|
|
156
|
+
return from && to ? `${shortDay(from)} – ${shortDay(to)}` : from ? `since ${shortDay(from)}` : `until ${shortDay(to)}`;
|
|
157
|
+
}
|
|
158
|
+
return whole ? v.map(full).join(", ") : v.length === 1 ? name(v[0]) : `${name(v[0])} +${v.length - 1}`;
|
|
159
|
+
}
|
|
160
|
+
function setFilter(key, v) {
|
|
161
|
+
S.f[key] = v; S.cursor = -1;
|
|
128
162
|
renderFilters(); renderList(); scrollTo(0, 0);
|
|
129
|
-
if (S.open && S.mode === "view") renderDrawer();
|
|
163
|
+
if (key === "kw" && S.open && S.mode === "view") renderDrawer();
|
|
130
164
|
}
|
|
165
|
+
const toggleValue = (key, v) => setFilter(key, S.f[key].includes(v) ? S.f[key].filter(x => x !== v) : [...S.f[key], v]);
|
|
166
|
+
function clearFilters() { S.f = noFilters(); S.menu = null; S.cursor = -1; renderFilters(); renderList(); }
|
|
167
|
+
const setKeyword = k => toggleValue("kw", k);
|
|
131
168
|
function keywordChips(m) {
|
|
132
169
|
if (!(m.keywords || []).length) return null;
|
|
133
|
-
return el("div", { class: "kws" }, m.keywords.map(k =>
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
return el("div", { class: "kws" }, m.keywords.map(k => {
|
|
171
|
+
const on = S.f.kw.includes(k.k);
|
|
172
|
+
return el("button", { class: "kw" + (on ? " on" : ""), text: k.t, title: on ? `Stop filtering by ${k.t}` : `Only memories that mention ${k.t}`,
|
|
173
|
+
onclick: e => { e.stopPropagation(); setKeyword(k.k); } });
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ------------------------------------------------------------ filter bar: one quiet menu per filter
|
|
178
|
+
function filterButton(d) {
|
|
179
|
+
const on = S.f[d.key].length > 0, open = S.menu === d.key, label = d.label.toLowerCase();
|
|
180
|
+
return el("span", { class: "fm" + (on ? " on" : "") + (open ? " open" : "") },
|
|
181
|
+
el("button", { class: "q", "aria-haspopup": "true", "aria-expanded": String(open), "data-focus": `q-${d.key}`, title: on ? `${d.label}: ${shownAs(d, true)}` : null,
|
|
182
|
+
onclick: e => { e.stopPropagation(); toggleMenu(d.key); } },
|
|
183
|
+
el("span", { class: "ql", text: d.label }), el("span", { class: "qv", text: shownAs(d) })),
|
|
184
|
+
on ? el("button", { class: "qx", title: `Clear ${label}`, "aria-label": `Clear ${label}`, text: "×",
|
|
185
|
+
onclick: e => { e.stopPropagation(); setFilter(d.key, d.single ? "" : []); } }) : null,
|
|
186
|
+
open ? filterMenu(d) : null);
|
|
187
|
+
}
|
|
188
|
+
function filterMenu(d) {
|
|
189
|
+
const menu = el("div", { class: "menu", role: "menu", "aria-label": d.label, onclick: e => e.stopPropagation() });
|
|
190
|
+
let items = d.options;
|
|
191
|
+
if (d.find) {
|
|
192
|
+
menu.append(el("input", { class: "mfind", type: "search", placeholder: "Find a keyword", value: S.menuFind, "data-focus": "mfind",
|
|
193
|
+
"aria-label": "Find a keyword", oninput: e => { S.menuFind = e.target.value; renderFilters(); } }));
|
|
194
|
+
const f = S.menuFind.trim().toLowerCase();
|
|
195
|
+
if (f) items = items.filter(([, t]) => t.toLowerCase().includes(f));
|
|
196
|
+
}
|
|
197
|
+
const list = el("div", { class: "mlist" }, items.map(([v, t, c]) => {
|
|
198
|
+
const checked = d.single ? S.f[d.key] === v : S.f[d.key].includes(v);
|
|
199
|
+
return el("button", { class: "mi" + (checked ? " on" : "") + (!c && !checked ? " zero" : ""), role: d.single ? "menuitemradio" : "menuitemcheckbox",
|
|
200
|
+
"aria-checked": String(checked), "data-focus": `mi-${d.key}-${v}`,
|
|
201
|
+
onclick: () => d.single ? setFilter(d.key, checked ? "" : v) : toggleValue(d.key, v) },
|
|
202
|
+
el("span", { class: "box" + (d.single ? " radio" : "") }), el("span", { class: "mt", text: t }), el("span", { class: "n", text: c }));
|
|
203
|
+
}));
|
|
204
|
+
if (!items.length) list.append(el("div", { class: "mnone", text: "No keyword matches" }));
|
|
205
|
+
menu.append(list);
|
|
206
|
+
if (d.key === "added") menu.append(dateRange());
|
|
207
|
+
if (S.f[d.key].length) menu.append(el("div", { class: "mfoot" },
|
|
208
|
+
el("button", { class: "clear", text: "Clear", onclick: () => setFilter(d.key, d.single ? "" : []) })));
|
|
209
|
+
return menu;
|
|
210
|
+
}
|
|
211
|
+
function dateRange() {
|
|
212
|
+
const [, from = "", to = ""] = S.f.added.startsWith("range:") ? S.f.added.split(":") : [];
|
|
213
|
+
const set = (a, b) => setFilter("added", a || b ? `range:${a}:${b}` : "");
|
|
214
|
+
return el("div", { class: "mrange" }, el("div", { class: "mh", text: "Or between" }),
|
|
215
|
+
el("div", { class: "dates" },
|
|
216
|
+
el("input", { type: "date", value: from, max: to || null, "aria-label": "Added on or after", "data-focus": "from", onchange: e => set(e.target.value, to) }),
|
|
217
|
+
el("span", { text: "–" }),
|
|
218
|
+
el("input", { type: "date", value: to, min: from || null, "aria-label": "Added on or before", "data-focus": "to", onchange: e => set(from, e.target.value) })));
|
|
219
|
+
}
|
|
220
|
+
function toggleMenu(key) {
|
|
221
|
+
S.menu = S.menu === key ? null : key; S.menuFind = "";
|
|
222
|
+
renderFilters();
|
|
223
|
+
const first = document.querySelector(".menu .mfind, .menu .mi");
|
|
224
|
+
if (first) first.focus();
|
|
225
|
+
}
|
|
226
|
+
function closeMenu() {
|
|
227
|
+
const key = S.menu;
|
|
228
|
+
S.menu = null; renderFilters();
|
|
229
|
+
const btn = document.querySelector(`[data-focus="q-${key}"]`);
|
|
230
|
+
if (btn) btn.focus();
|
|
231
|
+
}
|
|
232
|
+
// the order is chosen in the date column's header, whose label says which dates the column shows and which way they run
|
|
233
|
+
const SORT_LABEL = { new: "Added ↓", old: "Added ↑", edited: "Changed ↓", az: "A to Z" };
|
|
234
|
+
function renderSort() {
|
|
235
|
+
const col = $("date-col");
|
|
236
|
+
if (S.q) { col.replaceChildren("Added"); return; } // matches come in order of relevance
|
|
237
|
+
col.replaceChildren(el("span", { class: "fm sort", title: "Change the order" }, el("span", { class: "q" },
|
|
238
|
+
el("span", { class: "qv", text: SORT_LABEL[S.sort] || SORT_LABEL.new }),
|
|
239
|
+
el("select", { "aria-label": "Order", onchange: e => { S.sort = e.target.value; local.set("sort", S.sort); renderList(); } },
|
|
240
|
+
SORTS.map(([v, t]) => el("option", { value: v, text: t, selected: v === S.sort }))))));
|
|
136
241
|
}
|
|
137
242
|
function renderFilters() {
|
|
243
|
+
const was = document.activeElement && document.activeElement.dataset && document.activeElement.dataset.focus;
|
|
138
244
|
const box = $("filters");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
pill("kw", "Keyword", [["", "Any keyword"], ...keywordOptions()], S.f.kw, set("kw")),
|
|
149
|
-
pill("age", "When", [["", "Any time"], ...AGES], S.f.age, set("age")),
|
|
150
|
-
pill("fmt", "Format", [["", "Any format"], ["tidy", `Needs tidying · ${count("fmt", m => (m.issues || []).length)}`]], S.f.fmt, set("fmt")),
|
|
151
|
-
any ? el("button", { class: "clear", text: "Clear filters", onclick: () => { S.f = { type: "", loads: "", tool: "", age: "", kw: "", fmt: "" }; renderFilters(); renderList(); } }) : null,
|
|
152
|
-
el("span", { class: "spacer" }),
|
|
153
|
-
el("span", { class: "count", id: "count" }),
|
|
154
|
-
S.q ? null : pill("sort", "Order", SORTS, S.sort, v => { S.sort = v; local.set("sort", v); renderFilters(); renderList(); }),
|
|
155
|
-
].filter(Boolean));
|
|
156
|
-
}
|
|
245
|
+
box.replaceChildren(el("div", { class: "frow" }, filterDefs().map(filterButton),
|
|
246
|
+
filtering() > 1 ? el("button", { class: "clear", text: "Clear all", onclick: clearFilters }) : null));
|
|
247
|
+
// the bar is rebuilt on every change: put focus back where it was, and keep an open menu inside the window
|
|
248
|
+
const again = was && box.querySelector(`[data-focus="${CSS.escape(was)}"]`);
|
|
249
|
+
if (again) { again.focus(); if (again.type === "search") again.setSelectionRange(again.value.length, again.value.length); }
|
|
250
|
+
const menu = box.querySelector(".menu");
|
|
251
|
+
if (menu && menu.getBoundingClientRect().right > innerWidth - 8) menu.classList.add("right");
|
|
252
|
+
}
|
|
253
|
+
document.addEventListener("click", e => { if (S.menu && !e.target.closest(".fm")) { S.menu = null; renderFilters(); } });
|
|
157
254
|
|
|
158
255
|
// ------------------------------------------------------------ search
|
|
159
256
|
const q = $("q");
|
|
@@ -174,13 +271,12 @@
|
|
|
174
271
|
return scored.sort((a, b) => b[0] - a[0] || touched(M[b[1]]).localeCompare(touched(M[a[1]]))).map(([, i]) => i);
|
|
175
272
|
}
|
|
176
273
|
function runSearch(text, semantic = true) {
|
|
177
|
-
const was = S.q;
|
|
178
274
|
S.q = text.trim();
|
|
179
275
|
S.lex = S.q ? lexical(S.q) : [];
|
|
180
276
|
if (!S.q) S.sem = [];
|
|
181
277
|
S.cursor = S.q ? 0 : -1;
|
|
182
278
|
S.semPending = !!(semantic && S.q && D.semantic);
|
|
183
|
-
|
|
279
|
+
|
|
184
280
|
renderList();
|
|
185
281
|
clearTimeout(semTimer);
|
|
186
282
|
if (!S.semPending) return;
|
|
@@ -250,21 +346,24 @@
|
|
|
250
346
|
if (S.cursor >= S.rows.length) S.cursor = S.rows.length - 1;
|
|
251
347
|
const shown = S.rows.length, total = M.length;
|
|
252
348
|
const countEl = $("count");
|
|
253
|
-
|
|
349
|
+
S.countText = S.q ? `${plural(groups[0][1].length, "match", "matches")}${groups[1] ? ` + ${groups[1][1].length} close in meaning` : ""}${S.semPending ? " · searching by meaning…" : ""}`
|
|
254
350
|
: shown === total ? plural(total, "memory", "memories") : `${shown} of ${total}`;
|
|
351
|
+
if (countEl) countEl.textContent = S.countText;
|
|
352
|
+
renderSort();
|
|
255
353
|
|
|
256
354
|
list.classList.toggle("selecting", S.picked.size > 0);
|
|
257
|
-
|
|
355
|
+
const box = $("rows");
|
|
356
|
+
box.replaceChildren();
|
|
258
357
|
let k = 0;
|
|
259
358
|
for (const [title, idx] of groups) {
|
|
260
|
-
if (title && idx.length)
|
|
261
|
-
for (const i of idx)
|
|
359
|
+
if (title && idx.length) box.append(el("div", { class: "group", text: title }));
|
|
360
|
+
for (const i of idx) box.append(row(i, k++, title ? [] : ts));
|
|
262
361
|
}
|
|
263
362
|
if (!shown) {
|
|
264
|
-
const filtered =
|
|
265
|
-
|
|
363
|
+
const filtered = filtering() > 0;
|
|
364
|
+
box.append(el("div", { class: "empty" },
|
|
266
365
|
el("div", { text: S.q ? (S.semPending ? "No word matches; searching by meaning…" : "No matches.") : "No memories match these filters." }),
|
|
267
|
-
filtered ? el("button", { class: "btn", text: "Clear filters", onclick:
|
|
366
|
+
filtered ? el("button", { class: "btn", text: "Clear filters", onclick: clearFilters }) : null));
|
|
268
367
|
}
|
|
269
368
|
renderSelbar();
|
|
270
369
|
}
|
|
@@ -278,10 +377,9 @@
|
|
|
278
377
|
el("div", { class: "main" },
|
|
279
378
|
el("div", { class: "desc" }, highlight(m.description, ts)),
|
|
280
379
|
snip ? el("div", { class: "snip" }, highlight(snip, ts)) : keywordChips(m)),
|
|
281
|
-
el("
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
el("span", { class: "d", title: m.updated ? `added ${day(m.created)}, changed ${day(m.updated)}` : `added ${day(m.created)}`, text: day(S.sort === "edited" ? touched(m) : m.created) })));
|
|
380
|
+
el("span", { class: "c-type" }, el("i", { class: `dot ${m.type}` }), m.type),
|
|
381
|
+
el("span", { class: "c-place", title: m.scope === "global" ? "Loads in every session" : m.place, text: m.scope === "global" ? "everywhere" : m.place }),
|
|
382
|
+
el("span", { class: "c-date", title: m.updated ? `added ${day(m.created)}, changed ${day(m.updated)}` : `added ${day(m.created)}`, text: day(S.sort === "edited" ? touched(m) : m.created) }));
|
|
285
383
|
}
|
|
286
384
|
function moveCursor(step) {
|
|
287
385
|
if (!S.rows.length) return;
|
|
@@ -462,8 +560,8 @@
|
|
|
462
560
|
const show = v => Array.isArray(v) ? (v.length ? v.join(", ") : "[]") : v === null ? "null" : String(v);
|
|
463
561
|
for (const [k, v] of Object.entries(meta)) {
|
|
464
562
|
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
465
|
-
dl.append(el("div", { class: "
|
|
466
|
-
for (const [k2, v2] of Object.entries(v)) dl.append(el("dt", {
|
|
563
|
+
dl.append(el("div", { class: "sect", text: k }));
|
|
564
|
+
for (const [k2, v2] of Object.entries(v)) dl.append(el("dt", { text: k2 }), el("dd", { text: show(v2) }));
|
|
467
565
|
} else dl.append(el("dt", { text: k }), el("dd", { text: show(v) }));
|
|
468
566
|
}
|
|
469
567
|
return dl;
|
|
@@ -480,11 +578,13 @@
|
|
|
480
578
|
iconBtn("trash", "Delete", () => deleteMany([m.id])),
|
|
481
579
|
iconBtn("close", "Close (Esc)", () => closeDrawer())),
|
|
482
580
|
el("h1", { text: m.description }),
|
|
581
|
+
el("div", { class: "d-meta" },
|
|
582
|
+
el("span", { title: TYPE_HELP[m.type] }, el("i", { class: `dot ${m.type}` }), m.type),
|
|
583
|
+
el("span", { class: "sep", text: "·" }),
|
|
584
|
+
el("span", { text: m.scope === "global" ? "loads everywhere" : `loads in ${m.place || "one place"}` }),
|
|
585
|
+
el("span", { class: "sep", text: "·" }),
|
|
586
|
+
el("span", { text: m.updated ? `changed ${day(m.updated)}` : `added ${day(m.created)}` })),
|
|
483
587
|
keywordChips(m),
|
|
484
|
-
el("div", { class: "chips" },
|
|
485
|
-
el("span", { class: "chip", text: m.type }),
|
|
486
|
-
el("span", { class: "chip", text: m.scope === "global" ? "loads everywhere" : `loads in ${m.place || "one place"}` }),
|
|
487
|
-
el("span", { class: "chip", text: m.updated ? `changed ${day(m.updated)}` : `added ${day(m.created)}` })),
|
|
488
588
|
(m.issues || []).length ? el("ul", { class: "issues", title: "Differs from the format in PROTOCOL.md" }, m.issues.map(t => el("li", { text: t.replace(/\*\*([^*]+?):?\*\*/g, "“$1”") }))) : null,
|
|
489
589
|
el("div", { class: "tabs", role: "tablist" },
|
|
490
590
|
["rendered", "file"].map(t => el("button", { role: "tab", "aria-selected": String(S.tab === t), text: t === "rendered" ? "Memory" : "File",
|
|
@@ -492,9 +592,9 @@
|
|
|
492
592
|
const body = el("div", { class: "d-body" });
|
|
493
593
|
if (S.tab === "file") body.append(el("pre", { class: "raw", text: m.raw }));
|
|
494
594
|
else {
|
|
495
|
-
body.append(
|
|
595
|
+
body.append(markdown(m.body), el("div", { class: "section" }, el("h2", { text: "Details" }), propsList(m.meta)));
|
|
496
596
|
const sim = m.similar.filter(s => s.s > 0.05).slice(0, 5);
|
|
497
|
-
if (sim.length) body.append(el("div", { class: "similar" }, el("h2", { text: "Similar memories" }),
|
|
597
|
+
if (sim.length) body.append(el("div", { class: "section similar" }, el("h2", { text: "Similar memories" }),
|
|
498
598
|
sim.map(s => el("button", { onclick: () => pick(M[s.i].id) }, el("span", { class: "t", text: M[s.i].description }),
|
|
499
599
|
el("span", { class: "p", text: `${Math.round(s.s * 100)}%` })))));
|
|
500
600
|
}
|
|
@@ -543,7 +643,7 @@
|
|
|
543
643
|
iconBtn("close", "Cancel (Esc)", () => isNew ? closeDrawer() : cancelEdit()))),
|
|
544
644
|
el("div", { class: "d-body" }, el("div", { class: "form" },
|
|
545
645
|
el("label", {}, el("span", { style: "display:flex;justify-content:space-between" }, "Description", counter), desc),
|
|
546
|
-
el("div", { class: "
|
|
646
|
+
el("div", { class: "pair" }, el("label", {}, el("span", { style: "display:flex;justify-content:space-between" }, "Type", typeHint), typeSel),
|
|
547
647
|
el("label", {}, "Loads", seg)),
|
|
548
648
|
placeRow,
|
|
549
649
|
el("label", {}, "Memory (Markdown)", bodyArea))),
|
|
@@ -590,10 +690,13 @@
|
|
|
590
690
|
}
|
|
591
691
|
|
|
592
692
|
// ------------------------------------------------------------ chrome
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
693
|
+
function renderStatus() {
|
|
694
|
+
$("repo").textContent = D.dataDir || "";
|
|
695
|
+
$("repo").title = "Data repository";
|
|
696
|
+
const g = D.sync || {};
|
|
697
|
+
$("status").replaceChildren(g.remote && g.ahead
|
|
698
|
+
? el("span", { class: "warn", title: "Commits that a hook or this page made but has not pushed yet" }, el("i", { class: "dot" }), `${plural(g.ahead, "change")} not pushed`)
|
|
699
|
+
: "");
|
|
597
700
|
}
|
|
598
701
|
$("new").addEventListener("click", startNew);
|
|
599
702
|
document.addEventListener("keydown", e => {
|
|
@@ -602,6 +705,7 @@
|
|
|
602
705
|
e.preventDefault(); const btn = drawer.querySelector(".d-foot .btn.primary"); if (btn) btn.click(); return;
|
|
603
706
|
}
|
|
604
707
|
if (e.key === "Escape") {
|
|
708
|
+
if (S.menu) { closeMenu(); return; }
|
|
605
709
|
if (typing) { document.activeElement.blur(); return; }
|
|
606
710
|
if (S.mode === "edit") return cancelEdit();
|
|
607
711
|
if (S.open || S.mode === "new") return closeDrawer();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Memories · agent-memory</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' rx='6' fill='%23BA7517'/%3E%3Cpath d='M8 5.5h5.5L17 9v9.5H8z' fill='none' stroke='%23fff' stroke-width='1.6' stroke-linejoin='round'/%3E%3Cpath d='M10.5 12.5h4M10.5 15.5h3' stroke='%23fff' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E">
|
|
8
|
+
<link rel="stylesheet" href="/app.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="page">
|
|
12
|
+
<header class="head">
|
|
13
|
+
<div class="wrap">
|
|
14
|
+
<div class="brand">
|
|
15
|
+
<svg class="mark" viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6"/><path d="M8 5.5h5.5L17 9v9.5H8z"/><path d="M10.5 12.5h4M10.5 15.5h3"/></svg>
|
|
16
|
+
<span class="name">agent-memory</span>
|
|
17
|
+
<span class="repo" id="repo"></span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="status" id="status"></div>
|
|
20
|
+
<button class="btn primary" id="new" title="New memory (n)">New memory</button>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<div class="top">
|
|
25
|
+
<div class="wrap">
|
|
26
|
+
<div class="search">
|
|
27
|
+
<svg class="search-ico" viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.8"/><path d="m10.6 10.6 3.4 3.4"/></svg>
|
|
28
|
+
<input id="q" type="search" placeholder="Search memories" autocomplete="off" spellcheck="false" aria-label="Search memories">
|
|
29
|
+
<kbd>/</kbd>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="filters" id="filters"></div>
|
|
32
|
+
<div class="cols">
|
|
33
|
+
<span></span><span class="c-count" id="count" aria-live="polite"></span><span class="c-type">Type</span><span class="c-place">Loads in</span><span class="c-date" id="date-col"></span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<main class="list wrap" id="list" aria-label="Memories"><div id="rows"></div></main>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="selbar" id="selbar" hidden></div>
|
|
41
|
+
|
|
42
|
+
<aside class="drawer" id="drawer" aria-hidden="true"></aside>
|
|
43
|
+
<div class="toasts" id="toasts" aria-live="polite"></div>
|
|
44
|
+
<div class="boot" id="boot">Loading memories…</div>
|
|
45
|
+
<script src="/app.js"></script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/* Memories: a search box, a row of filters, the list, and one memory at a time in the drawer. */
|
|
2
|
-
:root {
|
|
3
|
-
color-scheme: light;
|
|
4
|
-
--page: #f7f7f5;
|
|
5
|
-
--surface: #ffffff;
|
|
6
|
-
--ink: #171716;
|
|
7
|
-
--ink-2: #57564f;
|
|
8
|
-
--ink-3: #8d8c85;
|
|
9
|
-
--line: #e6e5df;
|
|
10
|
-
--d1: #efeee9;
|
|
11
|
-
--accent: #2a78d6;
|
|
12
|
-
--accent-soft: rgba(42, 120, 214, 0.12);
|
|
13
|
-
--danger: #c93838;
|
|
14
|
-
--shadow: 0 12px 40px rgba(20, 20, 18, 0.14);
|
|
15
|
-
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
16
|
-
--t-user: #8a62cf; --t-feedback: #2a78d6; --t-project: #3c9a6b; --t-reference: #df8530;
|
|
17
|
-
--drawer: min(600px, 100vw);
|
|
18
|
-
}
|
|
19
|
-
@media (prefers-color-scheme: dark) {
|
|
20
|
-
:root {
|
|
21
|
-
color-scheme: dark;
|
|
22
|
-
--page: #121211;
|
|
23
|
-
--surface: #1b1b1a;
|
|
24
|
-
--ink: #f2f2ef;
|
|
25
|
-
--ink-2: #bcbbb3;
|
|
26
|
-
--ink-3: #85847d;
|
|
27
|
-
--line: #2b2b29;
|
|
28
|
-
--d1: #242422;
|
|
29
|
-
--accent: #4b93ea;
|
|
30
|
-
--accent-soft: rgba(75, 147, 234, 0.2);
|
|
31
|
-
--danger: #e66767;
|
|
32
|
-
--shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
|
|
33
|
-
--t-user: #a98ae6; --t-feedback: #6aa1ec; --t-project: #5fbf8e; --t-reference: #f0a45c;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
* { box-sizing: border-box; }
|
|
38
|
-
[hidden] { display: none !important; }
|
|
39
|
-
html, body { margin: 0; }
|
|
40
|
-
body { background: var(--page); color: var(--ink); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", "PingFang SC", sans-serif; }
|
|
41
|
-
button, input, select, textarea { font: inherit; color: inherit; }
|
|
42
|
-
button { cursor: pointer; }
|
|
43
|
-
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
44
|
-
|
|
45
|
-
.btn { border: 1px solid var(--line); background: var(--surface); border-radius: 10px; padding: 7px 14px; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
|
|
46
|
-
.btn:hover { background: var(--d1); }
|
|
47
|
-
.btn.primary { background: var(--ink); color: var(--surface); border-color: var(--ink); font-weight: 600; }
|
|
48
|
-
.btn.primary:hover { opacity: .9; background: var(--ink); }
|
|
49
|
-
.btn.danger { color: var(--danger); }
|
|
50
|
-
.btn:disabled { opacity: .45; cursor: default; }
|
|
51
|
-
.icon-btn { border: 0; background: none; width: 32px; height: 32px; border-radius: 8px; display: inline-grid; place-items: center; color: var(--ink-2); }
|
|
52
|
-
.icon-btn:hover { background: var(--d1); color: var(--ink); }
|
|
53
|
-
.icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
|
54
|
-
|
|
55
|
-
/* ---------- top: search, then filters */
|
|
56
|
-
.top { position: sticky; top: 0; z-index: 10; background: color-mix(in srgb, var(--page) 92%, transparent); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); }
|
|
57
|
-
.bar, .filters, .list { max-width: 1080px; margin: 0 auto; padding-left: 24px; padding-right: 24px; }
|
|
58
|
-
.bar { display: flex; align-items: center; gap: 14px; padding-top: 14px; }
|
|
59
|
-
.search { position: relative; flex: 1; }
|
|
60
|
-
.search input { width: 100%; height: 42px; padding: 0 40px 0 40px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface); outline: none; font-size: 15px; }
|
|
61
|
-
.search input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
62
|
-
.search input::-webkit-search-cancel-button { display: none; }
|
|
63
|
-
.search-ico { position: absolute; left: 14px; top: 13px; width: 16px; height: 16px; fill: none; stroke: var(--ink-3); stroke-width: 1.6; stroke-linecap: round; }
|
|
64
|
-
.search kbd { position: absolute; right: 12px; top: 11px; font: 11px var(--mono); color: var(--ink-3); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; }
|
|
65
|
-
.sync { font-size: 12px; color: var(--ink-3); white-space: nowrap; }
|
|
66
|
-
|
|
67
|
-
.filters { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 10px; padding-bottom: 10px; }
|
|
68
|
-
.pill { position: relative; display: inline-flex; align-items: center; }
|
|
69
|
-
.pill select { appearance: none; -webkit-appearance: none; border: 1px solid var(--line); background: var(--surface); border-radius: 999px; padding: 4px 26px 4px 11px; font-size: 13px; color: var(--ink-2); max-width: 230px; text-overflow: ellipsis; cursor: pointer; }
|
|
70
|
-
.pill select:hover { border-color: var(--ink-3); }
|
|
71
|
-
.pill::after { content: ""; position: absolute; right: 11px; top: 50%; width: 5px; height: 5px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: translateY(-70%) rotate(45deg); pointer-events: none; }
|
|
72
|
-
.pill.on select { border-color: var(--ink); color: var(--ink); font-weight: 600; }
|
|
73
|
-
.filters .clear { border: 0; background: none; color: var(--accent); font-size: 13px; padding: 4px 6px; }
|
|
74
|
-
.filters .spacer { flex: 1; }
|
|
75
|
-
.filters .count { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
76
|
-
|
|
77
|
-
/* ---------- the list */
|
|
78
|
-
.list { padding-top: 6px; padding-bottom: 120px; }
|
|
79
|
-
.group { font-size: 12px; font-weight: 600; color: var(--ink-3); padding: 18px 10px 6px; letter-spacing: .02em; }
|
|
80
|
-
.row { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: 12px; align-items: start; padding: 10px 10px; border-radius: 10px; cursor: pointer; }
|
|
81
|
-
.row + .row { border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent); }
|
|
82
|
-
.row:hover { background: var(--surface); }
|
|
83
|
-
.row.cursor { box-shadow: inset 0 0 0 1.5px var(--ink-3); }
|
|
84
|
-
.row.open { background: var(--accent-soft); }
|
|
85
|
-
.row.picked { background: var(--d1); }
|
|
86
|
-
.row .chk { margin: 3px 0 0; opacity: 0; cursor: pointer; }
|
|
87
|
-
.row:hover .chk, .row.picked .chk, .selecting .row .chk, .row.cursor .chk { opacity: 1; }
|
|
88
|
-
.row .main { min-width: 0; }
|
|
89
|
-
.row .desc { overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
90
|
-
.row .snip { font-size: 12.5px; color: var(--ink-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
91
|
-
.row mark { background: color-mix(in srgb, var(--accent) 22%, transparent); color: inherit; border-radius: 3px; padding: 0 1px; }
|
|
92
|
-
.kws { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
|
|
93
|
-
.kw { border: 0; background: var(--d1); color: var(--ink-2); font-size: 11.5px; line-height: 18px; border-radius: 6px; padding: 0 7px; }
|
|
94
|
-
.kw:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--ink); }
|
|
95
|
-
.kw.on { background: var(--ink); color: var(--surface); }
|
|
96
|
-
.d-head .kws { margin-top: 10px; }
|
|
97
|
-
.d-head .kws + .chips { margin-top: 8px; }
|
|
98
|
-
.issues { margin: 10px 0 0; padding: 7px 12px 7px 26px; border-radius: 8px; background: color-mix(in srgb, #d9822b 10%, transparent); color: var(--ink-2); font-size: 12.5px; }
|
|
99
|
-
.issues li { margin: 1px 0; }
|
|
100
|
-
.row .meta { display: flex; gap: 14px; font-size: 12px; color: var(--ink-3); white-space: nowrap; padding-top: 2px; }
|
|
101
|
-
.row .meta .t { display: inline-flex; align-items: center; gap: 6px; width: 78px; }
|
|
102
|
-
.row .meta .t i { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); }
|
|
103
|
-
.row .meta .t.user i { background: var(--t-user); } .row .meta .t.feedback i { background: var(--t-feedback); }
|
|
104
|
-
.row .meta .t.project i { background: var(--t-project); } .row .meta .t.reference i { background: var(--t-reference); }
|
|
105
|
-
.row .meta .p { width: 200px; overflow: hidden; text-overflow: ellipsis; }
|
|
106
|
-
.row .meta .d { width: 84px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
107
|
-
.empty { text-align: center; color: var(--ink-3); padding: 70px 0; }
|
|
108
|
-
.empty .btn { margin-top: 12px; }
|
|
109
|
-
|
|
110
|
-
.selbar { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); display: flex; gap: 8px; align-items: center; background: var(--ink); color: var(--surface); padding: 8px 8px 8px 16px; border-radius: 12px; box-shadow: var(--shadow); z-index: 20; }
|
|
111
|
-
.selbar .n { font-weight: 600; margin-right: 6px; }
|
|
112
|
-
.selbar .btn { background: transparent; color: var(--surface); border-color: color-mix(in srgb, var(--surface) 30%, transparent); }
|
|
113
|
-
.selbar .btn:hover { background: color-mix(in srgb, var(--surface) 14%, transparent); }
|
|
114
|
-
.selbar .btn.danger { background: var(--danger); border-color: var(--danger); color: #fff; }
|
|
115
|
-
|
|
116
|
-
/* with the drawer open on a wide window, the list moves over to stay readable */
|
|
117
|
-
@media (min-width: 1300px) {
|
|
118
|
-
body.drawer-open .bar, body.drawer-open .filters, body.drawer-open .list { margin-left: max(0px, calc((100vw - var(--drawer) - 1080px) / 2)); margin-right: var(--drawer); }
|
|
119
|
-
body.drawer-open .selbar { left: calc((100vw - var(--drawer)) / 2); }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/* ---------- drawer: one memory, everything in its file */
|
|
123
|
-
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(600px, 100vw); background: var(--surface); border-left: 1px solid var(--line); box-shadow: var(--shadow); transform: translateX(104%); transition: transform .22s ease; z-index: 30; display: flex; flex-direction: column; }
|
|
124
|
-
.drawer.open { transform: none; }
|
|
125
|
-
.d-head { padding: 18px 20px 14px; border-bottom: 1px solid var(--line); }
|
|
126
|
-
.d-bar { display: flex; align-items: center; gap: 4px; margin: -6px -8px 8px 0; }
|
|
127
|
-
.d-bar .where { flex: 1; font-size: 12px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
128
|
-
.d-head h1 { font-size: 18px; line-height: 1.4; margin: 0; font-weight: 650; overflow-wrap: anywhere; }
|
|
129
|
-
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
130
|
-
.chip { font-size: 12px; color: var(--ink-2); background: var(--d1); border-radius: 6px; padding: 2px 8px; }
|
|
131
|
-
.tabs { display: flex; gap: 2px; margin-top: 14px; }
|
|
132
|
-
.tabs button { border: 0; background: none; padding: 5px 10px; border-radius: 7px; font-size: 13px; color: var(--ink-3); }
|
|
133
|
-
.tabs button[aria-selected="true"] { background: var(--d1); color: var(--ink); font-weight: 600; }
|
|
134
|
-
.d-body { flex: 1; overflow-y: auto; padding: 18px 20px 40px; }
|
|
135
|
-
.props { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 5px 16px; font-size: 12.5px; margin: 0 0 20px; padding: 12px 14px; background: var(--page); border-radius: 10px; }
|
|
136
|
-
.props dt { color: var(--ink-3); white-space: nowrap; }
|
|
137
|
-
.props dt.sub { padding-left: 12px; }
|
|
138
|
-
.props dd { margin: 0; color: var(--ink-2); overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; }
|
|
139
|
-
.props .group { grid-column: 1 / -1; color: var(--ink-3); font-size: 11px; margin-top: 4px; }
|
|
140
|
-
pre.raw { white-space: pre-wrap; overflow-wrap: anywhere; font: 12.5px/1.6 var(--mono); background: var(--page); border-radius: 10px; padding: 14px; margin: 0; }
|
|
141
|
-
.similar { margin-top: 28px; }
|
|
142
|
-
.similar h2 { font-size: 12px; color: var(--ink-3); font-weight: 600; margin: 0 0 6px; letter-spacing: .02em; }
|
|
143
|
-
.similar button { display: flex; gap: 10px; width: 100%; text-align: left; border: 0; background: none; padding: 7px 8px; border-radius: 8px; align-items: baseline; }
|
|
144
|
-
.similar button:hover { background: var(--d1); }
|
|
145
|
-
.similar .t { flex: 1; overflow-wrap: anywhere; }
|
|
146
|
-
.similar .p { font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
147
|
-
|
|
148
|
-
/* rendered markdown */
|
|
149
|
-
.md { font-size: 14.5px; line-height: 1.65; overflow-wrap: anywhere; }
|
|
150
|
-
.md > :first-child { margin-top: 0; }
|
|
151
|
-
.md h1, .md h2, .md h3, .md h4 { line-height: 1.35; margin: 1.3em 0 .5em; }
|
|
152
|
-
.md h1 { font-size: 17px; } .md h2 { font-size: 15.5px; } .md h3, .md h4 { font-size: 14.5px; }
|
|
153
|
-
.md p { margin: .6em 0; }
|
|
154
|
-
.md ul, .md ol { padding-left: 1.4em; margin: .5em 0; }
|
|
155
|
-
.md li { margin: .2em 0; }
|
|
156
|
-
.md code { font: 12.5px var(--mono); background: var(--d1); padding: 1px 5px; border-radius: 5px; }
|
|
157
|
-
.md pre { background: var(--page); border-radius: 10px; padding: 12px 14px; overflow-x: auto; }
|
|
158
|
-
.md pre code { background: none; padding: 0; white-space: pre; }
|
|
159
|
-
.md blockquote { margin: .6em 0; padding: 2px 14px; border-left: 3px solid var(--line); color: var(--ink-2); }
|
|
160
|
-
.md table { border-collapse: collapse; font-size: 13px; margin: .8em 0; display: block; overflow-x: auto; }
|
|
161
|
-
.md th, .md td { border: 1px solid var(--line); padding: 5px 9px; text-align: left; vertical-align: top; }
|
|
162
|
-
.md hr { border: 0; border-top: 1px solid var(--line); margin: 1.2em 0; }
|
|
163
|
-
.md a { color: var(--accent); }
|
|
164
|
-
.md .wikilink { color: var(--accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
|
165
|
-
|
|
166
|
-
/* editor */
|
|
167
|
-
.form { display: grid; gap: 16px; }
|
|
168
|
-
.form label { display: grid; gap: 6px; font-size: 12px; color: var(--ink-3); font-weight: 600; }
|
|
169
|
-
.form input[type="text"], .form textarea, .form select { border: 1px solid var(--line); background: var(--surface); border-radius: 10px; padding: 9px 11px; font-weight: 400; color: var(--ink); font-size: 14px; }
|
|
170
|
-
.form textarea.desc { font-size: 15px; font-weight: 500; resize: vertical; min-height: 48px; }
|
|
171
|
-
.form textarea.body { font: 13px/1.6 var(--mono); min-height: 320px; resize: vertical; }
|
|
172
|
-
.form .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
173
|
-
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 10px; padding: 2px; gap: 2px; }
|
|
174
|
-
.seg button { border: 0; background: none; padding: 6px 12px; border-radius: 8px; font-size: 13px; color: var(--ink-2); font-weight: 400; }
|
|
175
|
-
.seg button[aria-pressed="true"] { background: var(--d1); color: var(--ink); font-weight: 600; }
|
|
176
|
-
.form .hint { font-weight: 400; color: var(--ink-3); }
|
|
177
|
-
.form .hint.over { color: var(--danger); }
|
|
178
|
-
.d-foot { padding: 12px 20px; border-top: 1px solid var(--line); display: flex; gap: 8px; justify-content: flex-end; align-items: center; }
|
|
179
|
-
.d-foot .err { flex: 1; color: var(--danger); font-size: 13px; }
|
|
180
|
-
|
|
181
|
-
.toasts { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); display: grid; gap: 8px; z-index: 40; }
|
|
182
|
-
.toast { background: var(--ink); color: var(--surface); border-radius: 10px; padding: 10px 14px; font-size: 13px; display: flex; align-items: center; gap: 16px; box-shadow: var(--shadow); }
|
|
183
|
-
.toast.error { background: var(--danger); color: #fff; }
|
|
184
|
-
.toast button { border: 0; background: none; color: inherit; font-weight: 700; padding: 0; text-decoration: underline; text-underline-offset: 2px; }
|
|
185
|
-
.boot { position: fixed; inset: 0; display: grid; place-items: center; background: var(--page); color: var(--ink-3); z-index: 50; text-align: center; padding: 24px; }
|
|
186
|
-
|
|
187
|
-
@media (max-width: 900px) { .row .meta .p { display: none; } }
|
|
188
|
-
@media (max-width: 760px) {
|
|
189
|
-
.bar, .filters, .list { padding-left: 12px; padding-right: 12px; }
|
|
190
|
-
.search kbd { display: none; }
|
|
191
|
-
.row .meta .t { display: none; }
|
|
192
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Memories</title>
|
|
7
|
-
<link rel="stylesheet" href="/app.css">
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<header class="top">
|
|
11
|
-
<div class="bar">
|
|
12
|
-
<div class="search">
|
|
13
|
-
<svg class="search-ico" viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.8"/><path d="m10.6 10.6 3.4 3.4"/></svg>
|
|
14
|
-
<input id="q" type="search" placeholder="Search memories" autocomplete="off" spellcheck="false" aria-label="Search memories">
|
|
15
|
-
<kbd>/</kbd>
|
|
16
|
-
</div>
|
|
17
|
-
<span class="sync" id="sync" hidden></span>
|
|
18
|
-
<button class="btn primary" id="new" title="New memory (n)">New</button>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="filters" id="filters"></div>
|
|
21
|
-
</header>
|
|
22
|
-
|
|
23
|
-
<main class="list" id="list" aria-label="Memories"></main>
|
|
24
|
-
<div class="selbar" id="selbar" hidden></div>
|
|
25
|
-
|
|
26
|
-
<aside class="drawer" id="drawer" aria-hidden="true"></aside>
|
|
27
|
-
<div class="toasts" id="toasts" aria-live="polite"></div>
|
|
28
|
-
<div class="boot" id="boot">Loading memories…</div>
|
|
29
|
-
<script src="/app.js"></script>
|
|
30
|
-
</body>
|
|
31
|
-
</html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|