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.
Files changed (31) hide show
  1. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/PKG-INFO +7 -5
  2. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/README.md +6 -4
  3. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/pyproject.toml +1 -1
  4. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/pyproject.toml.orig +1 -1
  5. agent_memory_git-0.1.3/src/agent_memory/webui/app.css +263 -0
  6. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/webui/app.js +180 -76
  7. agent_memory_git-0.1.3/src/agent_memory/webui/index.html +47 -0
  8. agent_memory_git-0.1.2/src/agent_memory/webui/app.css +0 -192
  9. agent_memory_git-0.1.2/src/agent_memory/webui/index.html +0 -31
  10. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/LICENSE +0 -0
  11. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/PROTOCOL.md +0 -0
  12. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/__init__.py +0 -0
  13. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/__main__.py +0 -0
  14. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/capture.py +0 -0
  15. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/cli.py +0 -0
  16. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/config.py +0 -0
  17. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/context.py +0 -0
  18. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/embed_worker.py +0 -0
  19. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/frontmatter.py +0 -0
  20. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/gitsync.py +0 -0
  21. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/hooks.py +0 -0
  22. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/importers.py +0 -0
  23. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/install.py +0 -0
  24. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/lint.py +0 -0
  25. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/select.py +0 -0
  26. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/sessions.py +0 -0
  27. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/store.py +0 -0
  28. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/ui.py +0 -0
  29. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/ui_server.py +0 -0
  30. {agent_memory_git-0.1.2 → agent_memory_git-0.1.3}/src/agent_memory/util.py +0 -0
  31. {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.2
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, age, and whether it needs tidying (what `mem lint`
135
- reports, also listed on the open memory); the order is newest, oldest, recently changed or A to Z.
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
- every frontmatter field (including each `source` entry), the body rendered as Markdown with
138
- `[[links]]` that open other memories, the file exactly as stored, and the most similar memories.
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, age, and whether it needs tidying (what `mem lint`
115
- reports, also listed on the open memory); the order is newest, oldest, recently changed or A to Z.
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
- every frontmatter field (including each `source` entry), the body rendered as Markdown with
118
- `[[links]]` that open other memories, the file exactly as stored, and the most similar memories.
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-memory-git"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Shared, git-backed memory for Claude Code and Codex."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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: "", loads: "", tool: "", age: "", kw: "", fmt: "" }, sort: local.get("sort", "new"),
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
- if (S.f.loads.startsWith("place:") && !(D.places || []).some(p => "place:" + p.key === S.f.loads)) S.f.loads = "";
85
- renderSync();
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 AGES = [["7", "Added in the past week"], ["30", "Added in the past month"], ["stale", "Unchanged for 60+ days"]];
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 = S.f, now = Date.now();
100
- if (skip !== "type" && f.type && m.type !== f.type) return false;
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
- function pill(key, label, options, value, onchange) {
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
- if (S.f.kw && !out.some(([k]) => k === S.f.kw)) out.unshift([S.f.kw, n.get(S.f.kw) || 0]);
124
- return out.map(([k, c]) => [k, `${kwName.get(k) || k} · ${c}`]);
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
- function setKeyword(k) {
127
- S.f.kw = S.f.kw === k ? "" : k; S.cursor = -1;
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 => el("button", { class: "kw" + (S.f.kw === k.k ? " on" : ""), text: k.t,
134
- title: S.f.kw === k.k ? "Show all memories again" : `Only memories that mention ${k.t}`,
135
- onclick: e => { e.stopPropagation(); setKeyword(k.k); } })));
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
- const count = (key, test) => M.filter(m => passes(m, key) && test(m)).length;
140
- const set = key => v => { S.f[key] = v; S.cursor = -1; renderFilters(); renderList(); scrollTo(0, 0); };
141
- const tools = [...new Set(M.map(m => m.tool).filter(Boolean))].sort();
142
- const any = Object.values(S.f).some(Boolean);
143
- box.replaceChildren(...[
144
- pill("type", "Type", [["", "All types"], ...TYPES.map(t => [t, `${t} · ${count("type", m => m.type === t)}`])], S.f.type, set("type")),
145
- pill("loads", "Where it loads", [["", "Loads anywhere"], ["global", `Everywhere · ${count("loads", m => m.scope === "global")}`],
146
- ...(D.places || []).map(p => ["place:" + p.key, `${p.key} · ${count("loads", m => m.scope === "here" && m.place === p.key)}`])], S.f.loads, set("loads")),
147
- tools.length > 1 ? pill("tool", "Written by", [["", "Any source"], ...tools.map(t => [t, `${t} · ${count("tool", m => m.tool === t)}`])], S.f.tool, set("tool")) : null,
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
- if (!!was !== !!S.q) renderFilters(); // the order menu only shows without a query
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
- if (countEl) countEl.textContent = S.q ? `${plural(groups[0][1].length, "match", "matches")}${groups[1] ? ` + ${groups[1][1].length} close in meaning` : ""}${S.semPending ? " · searching by meaning…" : ""}`
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
- list.replaceChildren();
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) list.append(el("div", { class: "group", text: title }));
261
- for (const i of idx) list.append(row(i, k++, title ? [] : ts));
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 = Object.values(S.f).some(Boolean);
265
- list.append(el("div", { class: "empty" },
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: () => { S.f = { type: "", loads: "", tool: "", age: "", kw: "", fmt: "" }; renderFilters(); renderList(); } }) : null));
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("div", { class: "meta" },
282
- el("span", { class: `t ${m.type}` }, el("i"), m.type),
283
- el("span", { class: "p", title: m.scope === "global" ? "Loads in every session" : m.place, text: m.scope === "global" ? "everywhere" : m.place }),
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: "group", text: k }));
466
- for (const [k2, v2] of Object.entries(v)) dl.append(el("dt", { class: "sub", text: k2 }), el("dd", { text: show(v2) }));
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(propsList(m.meta), markdown(m.body));
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: "row" }, el("label", {}, el("span", { style: "display:flex;justify-content:space-between" }, "Type", typeHint), typeSel),
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 renderSync() {
594
- const s = $("sync"), g = D.sync || {};
595
- s.hidden = !(g.remote && g.ahead);
596
- if (!s.hidden) s.textContent = `${plural(g.ahead, "change")} not pushed yet`;
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>