memoir-cli 3.14.0 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/PROJECT-HANDOFF.md +46 -0
- package/docs/PROJECT-MAP-TRIAL.md +149 -0
- package/package.json +1 -1
- package/src/work/ui/app.js +212 -19
- package/src/work/ui/index.html +26 -11
- package/src/work/ui/style.css +248 -3
package/docs/PROJECT-HANDOFF.md
CHANGED
|
@@ -18,6 +18,52 @@ This documents the local project handoff in `src/work/` and `bin/memoir-work.js`
|
|
|
18
18
|
|
|
19
19
|
## See and correct what Memoir remembers
|
|
20
20
|
|
|
21
|
+
Memoir remains a CLI and tool integration. This optional browser companion reads
|
|
22
|
+
the same project record; it is not a separate memory service. You can keep using
|
|
23
|
+
the CLI, Codex or Cursor without opening the page.
|
|
24
|
+
|
|
25
|
+
The default **Records** view shows open actions, saved answers and check evidence.
|
|
26
|
+
**Records** and **Map** share one workspace, search box and category navigation.
|
|
27
|
+
Switching views keeps the search and category. Records use readable rows with
|
|
28
|
+
short headings and expandable text; source history stays available on each entry.
|
|
29
|
+
|
|
30
|
+
The **Map** view connects the current branch's project entries. Select a
|
|
31
|
+
node to read its full text, source and history, and inspect connected entries.
|
|
32
|
+
Solid lines show project membership, explicit record references, or a named file
|
|
33
|
+
that a check declares as input. A file link does not prove the entry's claims.
|
|
34
|
+
Suggested links are off by default. Enable **Suggested links** to include possible
|
|
35
|
+
shared topics around a selected entry; dashed lines distinguish these word
|
|
36
|
+
matches from recorded references. Each suggestion explains its words and is not
|
|
37
|
+
saved as a relationship. This prototype does not infer
|
|
38
|
+
causes, automatically determine affected work, or use personal memory.
|
|
39
|
+
|
|
40
|
+
Search and category filters narrow the map. The overview shows up to six entries
|
|
41
|
+
around the project; selecting an entry centers it and shows up to six direct
|
|
42
|
+
neighbors, with recorded references first. Lines connect only to that center.
|
|
43
|
+
It computes connections within up to 120 entries, prioritizing matches and the
|
|
44
|
+
selected entry. Search covers all active entries, including older ones, but the
|
|
45
|
+
visible map and connection list are not exhaustive. No text leaves the browser
|
|
46
|
+
to generate these connections.
|
|
47
|
+
|
|
48
|
+
**Records** provides the overview and category lists for editing. The overview
|
|
49
|
+
shows every open action as a short row; **Details** opens its explanation and
|
|
50
|
+
controls. Saved answers and recent decisions start collapsed. Matching checks
|
|
51
|
+
remain available from the summary and Checks category; only checks needing
|
|
52
|
+
review appear in the overview. Long entries in category lists expand with
|
|
53
|
+
**Read full entry**; completed actions remain under **Next actions**. Both views
|
|
54
|
+
use the same correction controls and project record. **Connections** opens a
|
|
55
|
+
record in the map; **Open in Records** returns to that entry's category. Removed
|
|
56
|
+
items use the Records recovery list and do not enter the active map.
|
|
57
|
+
|
|
58
|
+
Selecting a map entry opens its details beside the map in wide windows and below
|
|
59
|
+
it in narrow windows. Keyboard focus follows the selected context. Saving from
|
|
60
|
+
either view selects the saved entry, clears the old search and opens its category
|
|
61
|
+
so a filter cannot hide a successful save. Both views search covered file paths as
|
|
62
|
+
well as record text and sources. Overview search includes all matching records,
|
|
63
|
+
including completed actions and goals, without the overview's two-per-group limit.
|
|
64
|
+
See [the project map trial](PROJECT-MAP-TRIAL.md) for
|
|
65
|
+
tested behavior and the limits of suggested connections.
|
|
66
|
+
|
|
21
67
|
Run `memoir work view` in your project (or `node bin/memoir.js work view`
|
|
22
68
|
from a source checkout), or ask the agent
|
|
23
69
|
“Open my Memoir project view.” The browser shows the current branch's answers,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Project map trial — 2026-09-06
|
|
2
|
+
|
|
3
|
+
The map is functional, but this trial does not establish that it is faster than
|
|
4
|
+
the Records view. It is useful for following explicit references to check
|
|
5
|
+
evidence. Direct answers are already easy to find in Records, and keyword
|
|
6
|
+
suggestions still include weak connections.
|
|
7
|
+
|
|
8
|
+
## Actual browser tasks
|
|
9
|
+
|
|
10
|
+
Tested against this checkout's real project record in the Codex in-app browser.
|
|
11
|
+
The starting ledger was revision 70, with 27 active entries. Existing answers and
|
|
12
|
+
check evidence were read before starting; no setup questions were repeated.
|
|
13
|
+
|
|
14
|
+
| Task | Map | Records | Finding |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| Find the saved privacy boundary | One node selection from the default map | Answer already present in Overview | No advantage for the map on this direct lookup |
|
|
17
|
+
| Read the complete saved publication blocker | One node selection | One **Read full entry** activation from Overview | Both expose the same saved blocker; external account state was not reverified |
|
|
18
|
+
| Find checks covering `docs/PROJECT-HANDOFF.md` | Evidence filter plus file search | Checks category plus file search | Records initially returned no result; fixed to search covered files too |
|
|
19
|
+
| Follow the documentation decision to its check | Search for the document, select the decision, follow **Recorded link** | Category/file search finds the check separately | The map exposes the reference and explains the file relationship |
|
|
20
|
+
| Decide which checks need another look | Linked receipt named changed UI files | The same receipt named the same changes | Changed inputs were visible; unchanged CLI/MCP receipts were reused |
|
|
21
|
+
|
|
22
|
+
These are observed control activations, excluding scrolling, switching views and
|
|
23
|
+
test resets. They are not timed human measurements or a randomized comparison.
|
|
24
|
+
The tester already knew the project. Repeated questions: zero in this task;
|
|
25
|
+
that observation is not proof of an improvement against an unassisted baseline.
|
|
26
|
+
|
|
27
|
+
## Problems reproduced and fixed
|
|
28
|
+
|
|
29
|
+
1. **Detail panel blocked the map and Reset.** At the actual 742px window width,
|
|
30
|
+
the fixed panel overlapped Reset; a hit test returned the panel. Below 950px,
|
|
31
|
+
details now follow the map in normal flow. Selecting a node focuses its
|
|
32
|
+
heading; Back returns focus to the project. Reset was retested successfully.
|
|
33
|
+
2. **Filtering lost keyboard focus.** After a category click, focus was on the
|
|
34
|
+
document body because the filter had been replaced. Focus now moves to the
|
|
35
|
+
replacement active filter. Verified in the real browser and regression test.
|
|
36
|
+
3. **File search disagreed between views.** Records omitted check input paths and
|
|
37
|
+
source text. Both views now use the same searchable fields. The actual
|
|
38
|
+
documentation-file search returns the same check in both views.
|
|
39
|
+
4. **A newly saved entry disappeared into the overview.** A real browser save
|
|
40
|
+
succeeded but did not select the new record; focus fell back to the body.
|
|
41
|
+
A regression test reproduced it. Map saves now select the saved entry and
|
|
42
|
+
return focus to its visible correction control. A second real creation and
|
|
43
|
+
correction verified this behavior. Long entries exposed another case: the
|
|
44
|
+
first button was a hidden expansion control. Focus now targets card actions
|
|
45
|
+
explicitly; the regression test includes a long entry.
|
|
46
|
+
5. **Confirmation text had poor contrast.** The dark map inherited light text
|
|
47
|
+
on the old pale notification background. Map success/error notices now have
|
|
48
|
+
explicit dark backgrounds and contrasting text. The success message's
|
|
49
|
+
computed text/background contrast was 10.70:1 in the actual browser.
|
|
50
|
+
|
|
51
|
+
## Persistence and boundaries
|
|
52
|
+
|
|
53
|
+
Two project-only trial observations were saved through the actual editor. The
|
|
54
|
+
result record was corrected, temporarily removed and restored through Records.
|
|
55
|
+
A separate process read agent resume after removal and confirmed the result was
|
|
56
|
+
absent. After restoration it recovered the corrected version and preserved
|
|
57
|
+
history. The original privacy and delivery answers remained at revisions 3 and
|
|
58
|
+
50. Existing project decisions, settings and private memory were not rewritten
|
|
59
|
+
for this test.
|
|
60
|
+
|
|
61
|
+
The map was checked at 390px, the normal 742px window, and 1280px. Details no
|
|
62
|
+
longer overlap the map, and no horizontal overflow was observed. Wide windows
|
|
63
|
+
retain a side panel. This is a focused layout/keyboard check, not a full
|
|
64
|
+
accessibility audit.
|
|
65
|
+
|
|
66
|
+
At this initial trial, the UI regression suite contained 21 scenarios, including map focus, saving,
|
|
67
|
+
consistent search, exact versus suggested links, older-entry search, and literal
|
|
68
|
+
rendering of synthetic hostile markup. The existing 14 API/security groups cover
|
|
69
|
+
authentication, cross-origin access, stale edits, removal/restore, secret/scope
|
|
70
|
+
rejection and unavailable execution routes. These targeted checks and the guide
|
|
71
|
+
check were captured in the local ledger after the relevant files changed.
|
|
72
|
+
Matching CLI/MCP and adversarial handoff receipts were reused. This was not a new
|
|
73
|
+
production penetration test or a fresh Cursor round trip.
|
|
74
|
+
|
|
75
|
+
## What remains to establish
|
|
76
|
+
|
|
77
|
+
- Keyword suggestions are noisy. The privacy answer was linked through generic
|
|
78
|
+
words such as “user”, “evidence” and “handoff”; the labels explain the match but
|
|
79
|
+
cannot make it useful. Evaluate suggestions against human-labeled relevant
|
|
80
|
+
pairs before treating the map as an intelligent project model.
|
|
81
|
+
- The graph computes relationships within at most 120 entries.
|
|
82
|
+
Search covers all active entries, but connections are not exhaustive.
|
|
83
|
+
- There is no saved relationship editor, automatic impact analysis, natural
|
|
84
|
+
language retrieval, or measured reduction in time/repeated work from the map.
|
|
85
|
+
- Keep both views available. Next, test unfamiliar users on real project
|
|
86
|
+
questions and count successful answers, mistaken interpretations and time.
|
|
87
|
+
|
|
88
|
+
The trial used local, uncommitted changes. No publication, server deployment,
|
|
89
|
+
new account setup or personal-memory import was needed to perform it.
|
|
90
|
+
|
|
91
|
+
## Follow-up: reduce graph clutter
|
|
92
|
+
|
|
93
|
+
The user found the graph messy after this trial. The observed overview contained
|
|
94
|
+
24 lines and 11 nodes. The revised view shows six entries around the project,
|
|
95
|
+
with six straight membership lines and no lines between the surrounding entries.
|
|
96
|
+
Selecting an entry moves it to the center and shows only its direct neighbors,
|
|
97
|
+
with at most six links. Labels have larger type and a two-line limit; full text
|
|
98
|
+
remains in the inspector. Suggested links are now off by default and explicitly
|
|
99
|
+
enabled with **Suggested links**. The graph and inspector apply the same filter.
|
|
100
|
+
|
|
101
|
+
The actual browser showed six lines and seven nodes in the new overview. A check
|
|
102
|
+
with eight recorded connections displayed six; selecting an answer with no
|
|
103
|
+
recorded references showed zero links, then six dashed links after enabling
|
|
104
|
+
suggestions. At 390px, the seven node boxes did not overlap and the page had no
|
|
105
|
+
horizontal overflow. Two new regression cases cover the overview limit and
|
|
106
|
+
direct-neighbor filtering with opt-in suggestions, bringing the UI suite to 23
|
|
107
|
+
scenarios. The underlying keyword-matching quality is unchanged by this cleanup.
|
|
108
|
+
# Shared workspace follow-up — 6 September 2026
|
|
109
|
+
|
|
110
|
+
Further feedback was that even the unified page required too much reading.
|
|
111
|
+
The overview now shows all open actions as short rows with a **Details** button.
|
|
112
|
+
Saved answers and recent decisions start collapsed; matching check receipts are
|
|
113
|
+
available through the summary and Checks category. Only checks needing review
|
|
114
|
+
appear in the overview. Existing action text was shortened and the explanation
|
|
115
|
+
moved into its rationale; previous full text remains in local history. The npm
|
|
116
|
+
action now reflects the observed password-confirmation screen in Chrome.
|
|
117
|
+
|
|
118
|
+
The user reported that Map and Records still looked incoherent. The browser now
|
|
119
|
+
opens Records first and uses one header, search, category navigation, color
|
|
120
|
+
palette and correction dialog for both views. Records have a single column of
|
|
121
|
+
readable rows. The former separate sidebar, duplicate introductions and light
|
|
122
|
+
card grid are removed.
|
|
123
|
+
|
|
124
|
+
Switching views preserves search and category. **Connections** opens the same
|
|
125
|
+
record in the map; **Open in Records** returns to its category. A successful save
|
|
126
|
+
reveals the saved entry and clears filters that could hide it. Overview search
|
|
127
|
+
now includes every matching entry, including goals and completed actions.
|
|
128
|
+
|
|
129
|
+
Observed in the actual local browser:
|
|
130
|
+
|
|
131
|
+
- Found the existing privacy answer, opened its correction dialog and cancelled
|
|
132
|
+
without changing the answer, followed Connections and returned to Records.
|
|
133
|
+
- Searched `src/work/ui/style.css` and found the same three check receipts in
|
|
134
|
+
Records and Map, including specific changed-input warnings.
|
|
135
|
+
- A search with no results stayed empty after switching views.
|
|
136
|
+
- Created the actual workspace-design decision while an empty search was active.
|
|
137
|
+
Saving cleared the search, opened Decisions and showed the saved entry.
|
|
138
|
+
- Corrected that decision from Map and confirmed keyboard focus returned to
|
|
139
|
+
the visible Correct button. Returning to Records showed the correction and
|
|
140
|
+
its earlier version. Focus restoration now scopes itself to the active view.
|
|
141
|
+
- Inspected Records at the normal window width and 390px, Map at 1280px, and the
|
|
142
|
+
shared dark editor at 1280px. At 390px the seven overview node boxes did not
|
|
143
|
+
overlap; the document had no horizontal overflow. Narrow category navigation
|
|
144
|
+
scrolls horizontally, and map details follow the graph in normal flow.
|
|
145
|
+
|
|
146
|
+
The changed UI requires targeted UI/API and documentation rechecks. Unchanged
|
|
147
|
+
CLI/MCP handoff and adversarial receipts remain reusable. This is local design
|
|
148
|
+
and functional validation; it adds no independent-user or timed utility study.
|
|
149
|
+
The npm publishing step remains separate from this local preview.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/work/ui/app.js
CHANGED
|
@@ -6,11 +6,141 @@ let token = fragment.get('token');
|
|
|
6
6
|
try { if (token) sessionStorage.setItem('memoir-view-token', token); else token = sessionStorage.getItem('memoir-view-token'); } catch {}
|
|
7
7
|
if (fragment.has('token')) history.replaceState(null, '', '/');
|
|
8
8
|
let state, selected = 'overview', editing, latestEdit, editorOpener, busy = false, stateRequest = 0, renderAfterEditor = false;
|
|
9
|
-
|
|
10
|
-
const
|
|
9
|
+
let viewMode = 'records', focusKey = 'project', showSuggestions = false;
|
|
10
|
+
const labels = { overview: 'Overview', next: 'Next actions', answer: 'Answers', decision: 'Decisions', check: 'Checks', goal: 'Goals', removed: 'Removed' };
|
|
11
|
+
const descriptions = { overview: 'What matters for your next session.', answer: 'Already answered. Ready to carry forward.', decision: 'What you decided, with the reasons behind it.', check: 'Completed checks and the changes that need another look.', next: 'Open work first. Completed actions stay in the record.', goal: 'What this project is working toward.', removed: 'Hidden from the handoff. You can restore records here.' };
|
|
11
12
|
function el(tag, text, className) { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; }
|
|
12
13
|
function button(text, action, className = '') { const node = el('button', text, className); node.type = 'button'; node.addEventListener('click', action); return node; }
|
|
13
14
|
function date(value) { return new Date(value).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }); }
|
|
15
|
+
function searchableText(item) { return [item.text,item.answer,item.title,item.why,item.source,item.id,...Object.keys(item.inputs||{})].filter(Boolean).join(' ').toLowerCase(); }
|
|
16
|
+
function entryLabel(item) {
|
|
17
|
+
const text = item.kind === 'next' || /^record\.[0-9a-f-]{36}$/i.test(item.id) ? item.text.split(/(?<=[.!?])\s/)[0] : item.id.replace(/^(?:record|decision|answer|check|next|goal)\./, '').replace(/[._-]+/g, ' ').replace(/^./, c => c.toUpperCase()).replace(/\bcli\b/ig, 'CLI');
|
|
18
|
+
return text.length > 62 ? text.slice(0,59).trimEnd() + '…' : text;
|
|
19
|
+
}
|
|
20
|
+
// A bounded, local projection of existing records. Shared words are suggestions,
|
|
21
|
+
// never saved relationships, causal claims, or proof that a decision is correct.
|
|
22
|
+
function buildProjectMap(data, { query = '', focus = 'project', kind = 'all' } = {}) {
|
|
23
|
+
const body = item => [item.text, item.answer, item.title, item.why, item.source].filter(Boolean).join(' ');
|
|
24
|
+
const label = entryLabel;
|
|
25
|
+
const all = [...data.records.map(item => ({key:'record:'+item.id,kind:item.kind,item,label:label(item)})), ...data.checks.map(item => ({key:'check:'+item.id,kind:'check',item,label:label(item)}))].sort((a,b) => b.item.revision-a.item.revision);
|
|
26
|
+
const search = query.toLowerCase().trim();
|
|
27
|
+
const matches = node => (kind==='all'||node.kind===kind) && (!search || searchableText(node.item).includes(search));
|
|
28
|
+
const ordered = [...all].sort((a,b) => (b.key===focus)-(a.key===focus) || Number(matches(b))-Number(matches(a)));
|
|
29
|
+
const nodes = ordered.slice(0,120), edges = [];
|
|
30
|
+
for (const node of nodes) edges.push({source:'project',target:node.key,type:'recorded',label:'Stored in this project on the current branch.'});
|
|
31
|
+
const stop = new Set('about after again also already another before being branch checks completed context current decisions during existing files first found from have into local memoir memory needs next only passed personal project record records release review saved session should source state test tests that their these they this through using verified version were what when which will with work workflow would'.split(' '));
|
|
32
|
+
const terms = new Map(nodes.map(node => [node.key,new Set((body(node.item).normalize('NFKC').toLowerCase().match(/[\p{L}\p{N}]+/gu)||[]).filter(word => word.length>=4 && /\p{L}/u.test(word) && !stop.has(word)).slice(0,180))]));
|
|
33
|
+
const frequency = new Map(); for (const set of terms.values()) for (const term of set) frequency.set(term,(frequency.get(term)||0)+1);
|
|
34
|
+
const references=new Map(nodes.map(node=>[node.key,new Set((body(node.item).match(/[A-Za-z0-9][A-Za-z0-9._/-]*/g)||[]).map(token=>token.replace(/\.+$/,'')))]));
|
|
35
|
+
for (let i=0;i<nodes.length;i++) for (let j=i+1;j<nodes.length;j++) {
|
|
36
|
+
const a=nodes[i], b=nodes[j];
|
|
37
|
+
const reference = references.get(a.key).has(b.item.id) ? [a,b] : references.get(b.key).has(a.item.id) ? [b,a] : null;
|
|
38
|
+
if (reference) { edges.push({source:a.key,target:b.key,type:'recorded',label:reference[0].label+' explicitly mentions '+reference[1].item.id+'.'}); continue; }
|
|
39
|
+
const receipt = a.kind==='check'?a:b.kind==='check'?b:null, note=receipt===a?b:a;
|
|
40
|
+
const paths = receipt && note.kind!=='check' ? Object.keys(receipt.item.inputs||{}).filter(file=>references.get(note.key).has(file)) : [];
|
|
41
|
+
if (paths.length) { edges.push({source:a.key,target:b.key,type:'recorded',label:'This entry names '+paths.slice(0,3).join(', ')+'. The check declares '+(paths.length===1?'that file':'those files')+' as input; this does not verify the entry’s claims.'}); continue; }
|
|
42
|
+
const shared=[...terms.get(a.key)].filter(word=>terms.get(b.key).has(word) && frequency.get(word)<=Math.max(3,nodes.length*.6));
|
|
43
|
+
if (shared.length>=3) {
|
|
44
|
+
shared.sort((x,y)=>frequency.get(x)-frequency.get(y)||x.localeCompare(y));
|
|
45
|
+
edges.push({source:a.key,target:b.key,type:'suggested',score:shared.reduce((sum,word)=>sum+1/frequency.get(word),0),label:'Suggested from shared words: '+shared.slice(0,5).join(', ')+'. This is not a saved relationship.'});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {nodes,edges,total:all.length,matches:nodes.filter(matches),matchingTotal:all.filter(matches).length};
|
|
49
|
+
}
|
|
50
|
+
function mapNeighborhood(model, focus, suggestions = false, overview = false) {
|
|
51
|
+
const linked = focus ? model.edges.filter(edge => edge.source !== 'project' && edge.target !== 'project' &&
|
|
52
|
+
(edge.source === focus.key || edge.target === focus.key) && (suggestions || edge.type === 'recorded'))
|
|
53
|
+
.sort((a,b) => (a.type === 'suggested') - (b.type === 'suggested') || (b.score || 0) - (a.score || 0)) : [];
|
|
54
|
+
let ranked = focus ? linked.map(edge => model.nodes.find(node => node.key === (edge.source === focus.key ? edge.target : edge.source))) : [...model.matches];
|
|
55
|
+
if (!focus && overview) {
|
|
56
|
+
const picked = [];
|
|
57
|
+
for (const kind of ['goal','next','answer','decision','check']) {
|
|
58
|
+
const entry = ranked.find(node => node.kind === kind && (kind !== 'next' || node.item.status !== 'done'));
|
|
59
|
+
if (entry) picked.push(entry);
|
|
60
|
+
}
|
|
61
|
+
ranked = [...picked,...ranked.filter(node => !picked.includes(node))];
|
|
62
|
+
}
|
|
63
|
+
const shown = ranked.slice(0,6), visibleKeys = new Set(shown.map(node => node.key));
|
|
64
|
+
const edges = focus ? linked.filter(edge => visibleKeys.has(edge.source) || visibleKeys.has(edge.target)) :
|
|
65
|
+
model.edges.filter(edge => edge.source === 'project' && visibleKeys.has(edge.target));
|
|
66
|
+
return {ranked,shown,edges,linked};
|
|
67
|
+
}
|
|
68
|
+
function selectMapNode(key) {
|
|
69
|
+
focusKey=key; renderMap(true);
|
|
70
|
+
}
|
|
71
|
+
function renderMap(restoreFocus = false) {
|
|
72
|
+
if (!state) return;
|
|
73
|
+
const mapKind = selected === 'overview' || selected === 'removed' ? 'all' : selected;
|
|
74
|
+
const model=buildProjectMap(state,{query:$('search').value,focus:focusKey,kind:mapKind});
|
|
75
|
+
if (focusKey!=='project'&&!model.nodes.some(node=>node.key===focusKey)) focusKey='project';
|
|
76
|
+
const focus=model.nodes.find(node=>node.key===focusKey);
|
|
77
|
+
$('map-workspace').dataset.focused=String(!!focus);
|
|
78
|
+
const {ranked,shown,edges,linked}=mapNeighborhood(model,focus,showSuggestions,!$('search').value.trim()&&mapKind==='all');
|
|
79
|
+
$('map-count').textContent=focus ? shown.length+' of '+ranked.length+' connections · '+focus.label :
|
|
80
|
+
shown.length+' of '+model.matchingTotal+' entries'+(model.total>120?' · search to find older entries':'');
|
|
81
|
+
$('map-suggestions').setAttribute('aria-pressed',String(showSuggestions));
|
|
82
|
+
$('map-suggested-legend').hidden=!showSuggestions;
|
|
83
|
+
const canvas=$('map-canvas');canvas.replaceChildren();
|
|
84
|
+
const positions=[[22,18],[78,18],[19,47],[81,47],[22,77],[78,77]];
|
|
85
|
+
const center=focus||{key:'project',kind:'project',label:state.project_name};
|
|
86
|
+
const points=new Map([[center.key,[50,47]],...shown.map((node,index)=>[node.key,positions[index]])]);
|
|
87
|
+
const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 1000 640');svg.setAttribute('preserveAspectRatio','none');svg.setAttribute('aria-hidden','true');svg.setAttribute('class','map-lines');
|
|
88
|
+
for(const edge of edges) {
|
|
89
|
+
const a=points.get(edge.source),b=points.get(edge.target),line=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
90
|
+
line.setAttribute('d',`M ${a[0]*10} ${a[1]*6.4} L ${b[0]*10} ${b[1]*6.4}`);
|
|
91
|
+
line.setAttribute('class','map-line '+edge.type+((edge.source===focusKey||edge.target===focusKey)&&focusKey!=='project'?' focused':''));svg.append(line);
|
|
92
|
+
}
|
|
93
|
+
canvas.append(svg);
|
|
94
|
+
let focusedButton;
|
|
95
|
+
for(const node of [center,...shown]) {
|
|
96
|
+
const active=node.key===focusKey;
|
|
97
|
+
const entry=button('',()=>selectMapNode(node.key),'map-node'+(active?' selected':''));
|
|
98
|
+
entry.dataset.center=String(node.key===center.key);
|
|
99
|
+
entry.dataset.kind=node.kind;entry.dataset.nodeKey=node.key;entry.style.left=points.get(node.key)[0]+'%';entry.style.top=points.get(node.key)[1]+'%';entry.setAttribute('aria-pressed',String(active));entry.setAttribute('aria-label',(node.kind==='project'?'Project':labels[node.kind])+': '+(node.item?.text||node.item?.title||node.label));
|
|
100
|
+
const dot=el('span',undefined,'node-dot');dot.setAttribute('aria-hidden','true');
|
|
101
|
+
entry.append(dot,el('span',node.label,'node-label'),el('span',node.kind==='project'?model.total+' entries':node.kind==='check'?(node.item.freshness==='inputs-match'?'Inputs match':'Needs review'):node.kind==='next'?(node.item.status==='done'?'Completed':'Next action'):labels[node.kind],'node-kind'));
|
|
102
|
+
canvas.append(entry);if(active)focusedButton=entry;
|
|
103
|
+
}
|
|
104
|
+
if(!shown.length)canvas.append(el('p',focus?(showSuggestions?'No connected entries found.':'No recorded connections yet. Try Suggested links.'):$('search').value?'No matching entries. Try another term.':'No entries in this category yet.','map-empty'));
|
|
105
|
+
const panel=$('map-inspector');panel.replaceChildren();
|
|
106
|
+
if(focus)panel.append(button('← Back to project',()=>{focusKey='project';renderMap(true);},'inspector-back'));
|
|
107
|
+
const panelTitle=el('h2',focus?focus.label:state.project_name);panelTitle.tabIndex=-1;
|
|
108
|
+
panel.append(el('p',focus?'IN FOCUS':'PROJECT BRIEF','inspector-eyebrow'),panelTitle);
|
|
109
|
+
if(focus) {
|
|
110
|
+
const card=focus.kind==='check'?checkCard(focus.item):recordCard(focus.item);card.className+=' expanded';panel.append(card);
|
|
111
|
+
panel.append(button('Open in Records ↗', () => { selected = focus.kind; $('search').value = ''; setView('records'); $('section-title').focus(); }, 'inspector-back'));
|
|
112
|
+
const connections=el('div',undefined,'inspector-connections');connections.append(el('h3','Connected context'));
|
|
113
|
+
const relevant=linked.slice(0,6);
|
|
114
|
+
if(!relevant.length)connections.append(el('p',showSuggestions?'No recorded references or shared topics found.':'No recorded references found. Suggested links can reveal possible shared topics.','connection-note'));
|
|
115
|
+
for(const edge of relevant) {
|
|
116
|
+
const other=model.nodes.find(node=>node.key===(edge.source===focusKey?edge.target:edge.source));
|
|
117
|
+
const row=button('',()=>selectMapNode(other.key),'connection-row');
|
|
118
|
+
row.append(el('span',edge.type==='recorded'?'RECORDED LINK':'SUGGESTED LINK','connection-type '+edge.type),el('strong',other.label),el('span',edge.label,'connection-reason'));connections.append(row);
|
|
119
|
+
}
|
|
120
|
+
if(linked.length>6)connections.append(el('p','Showing 6 of '+linked.length+' connections. Search for another entry to explore more.','connection-note'));
|
|
121
|
+
panel.append(connections);
|
|
122
|
+
} else {
|
|
123
|
+
const goal=state.records.filter(item=>item.kind==='goal').sort((a,b)=>b.revision-a.revision)[0];
|
|
124
|
+
panel.append(el('p',goal?.text||'Start with an answer, a decision or a next action. Connections appear as your project record grows.','project-brief'));
|
|
125
|
+
panel.append(el('p','Select an entry to read the full context and see why it is connected.','inspector-hint'));
|
|
126
|
+
panel.append(button('+ Add project context',()=>edit(), 'map-add'));
|
|
127
|
+
const index=el('div',undefined,'inspector-index');index.append(el('h3',$('search').value?'Search results':mapKind==='all'?'Project entries':labels[mapKind]));
|
|
128
|
+
for(const node of ranked.slice(0,24)) {const row=button('',()=>selectMapNode(node.key),'index-entry');row.dataset.kind=node.kind;row.setAttribute('aria-label',labels[node.kind]+': '+node.label);row.append(el('i'),el('span',node.label),el('span','↗'));index.append(row);}
|
|
129
|
+
if(ranked.length>24)index.append(el('p','Search to narrow '+ranked.length+' matching entries.','connection-note'));
|
|
130
|
+
panel.append(index);
|
|
131
|
+
}
|
|
132
|
+
panel.scrollTop=0;
|
|
133
|
+
// In narrow windows the detail panel follows the map in normal flow. Move
|
|
134
|
+
// focus to its heading so the selected context is visible without an overlay.
|
|
135
|
+
if(restoreFocus)(focus?panelTitle:focusedButton)?.focus();
|
|
136
|
+
}
|
|
137
|
+
function setView(mode) {
|
|
138
|
+
viewMode=mode;
|
|
139
|
+
if (mode === 'map' && selected === 'removed') { selected = 'overview'; focusKey = 'project'; }
|
|
140
|
+
$('map-workspace').hidden=mode!=='map';$('records-workspace').hidden=mode!=='records';
|
|
141
|
+
$('show-map').setAttribute('aria-pressed',String(mode==='map'));$('show-records').setAttribute('aria-pressed',String(mode==='records'));
|
|
142
|
+
render();
|
|
143
|
+
}
|
|
14
144
|
function notice(message, error = false, undo) {
|
|
15
145
|
$('notice').replaceChildren(); const node = el('div', undefined, 'notice' + (error ? ' error' : '')); node.append(el('span', message));
|
|
16
146
|
if (undo) node.append(button('Undo', undo)); $('notice').append(node);
|
|
@@ -43,11 +173,27 @@ function nav() {
|
|
|
43
173
|
$('navigation').replaceChildren();
|
|
44
174
|
for (const [key, label] of Object.entries(labels)) {
|
|
45
175
|
const count = key === 'overview' ? null : key === 'removed' ? state.removed.length : key === 'check' ? state.checks.length : state.records.filter(r => r.kind === key).length;
|
|
46
|
-
const node = button(
|
|
176
|
+
const node = button('', () => { selected = key; focusKey = 'project'; if (key === 'removed') setView('records'); else render(); [...$('navigation').children].find(entry => entry.dataset.kind === key)?.focus({preventScroll:true}); });
|
|
177
|
+
node.dataset.kind = key;
|
|
178
|
+
const icon = el('span', undefined, 'nav-icon icon-' + key); icon.setAttribute('aria-hidden', 'true'); node.append(icon, el('span', label, 'nav-name'));
|
|
47
179
|
if (selected === key) node.setAttribute('aria-current', 'page');
|
|
48
180
|
if (count !== null) node.append(el('span', count, 'count')); $('navigation').append(node);
|
|
49
181
|
}
|
|
50
182
|
}
|
|
183
|
+
function summary() {
|
|
184
|
+
const stale = state.checks.filter(r => r.freshness !== 'inputs-match').length;
|
|
185
|
+
const entries = [
|
|
186
|
+
['next', state.records.filter(r => r.kind === 'next' && r.status !== 'done').length, 'Open actions'],
|
|
187
|
+
['answer', state.records.filter(r => r.kind === 'answer').length, 'Answers saved'],
|
|
188
|
+
['check', stale || state.checks.length, stale ? 'Checks to review' : 'Checks match files'],
|
|
189
|
+
];
|
|
190
|
+
$('summary').replaceChildren();
|
|
191
|
+
for (const [kind, count, label] of entries) {
|
|
192
|
+
const node = button('', () => { selected = kind; focusKey = 'project'; $('search').value = ''; render(); $('section-title').focus(); }, 'summary-item' + (kind === 'check' && stale ? ' needs-attention' : ''));
|
|
193
|
+
const arrow = el('span', '↗', 'summary-arrow'); arrow.setAttribute('aria-hidden', 'true');
|
|
194
|
+
node.append(el('strong', count), el('span', label), arrow); $('summary').append(node);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
51
197
|
function metadata(item, check = false) {
|
|
52
198
|
const details = el('details', undefined, 'metadata'); details.append(el('summary', check ? 'Evidence and covered files' : 'Source and earlier versions'));
|
|
53
199
|
details.append(el('p', `Saved ${date(item.recorded_at)} · revision ${item.revision}`));
|
|
@@ -56,7 +202,7 @@ function metadata(item, check = false) {
|
|
|
56
202
|
const list = el('ul'); Object.keys(item.inputs).forEach(file => list.append(el('li', file))); details.append(list);
|
|
57
203
|
details.append(el('p', 'Output was discarded. Its fingerprint:')); details.append(el('code', item.output_sha256));
|
|
58
204
|
} else {
|
|
59
|
-
details.append(el('p', item.source)); if (item.why) details.append(el('p', 'Why: ' + item.why));
|
|
205
|
+
details.append(el('p', item.source)); if (item.why && item.kind !== 'next') details.append(el('p', 'Why: ' + item.why));
|
|
60
206
|
const history = state.history.filter(r => r.id === item.id && r.revision < item.revision).reverse();
|
|
61
207
|
for (const old of history) { const row = el('p', `Revision ${old.revision} · ${date(old.recorded_at)}\n${old.text}${old.answer ? '\n' + old.answer : ''}\nSource: ${old.source}`); details.append(row); }
|
|
62
208
|
if (!history.length) details.append(el('p', 'No earlier versions.'));
|
|
@@ -67,14 +213,28 @@ function recordCard(item, removed = false) {
|
|
|
67
213
|
const card = el('article', undefined, 'card' + (item.kind === 'next' && item.status === 'done' ? ' done' : ''));
|
|
68
214
|
card.dataset.recordId = item.id;
|
|
69
215
|
const kind = item.kind === 'next' ? (item.status === 'done' ? 'DONE' : 'TO DO') : item.kind === 'answer' ? 'ANSWERED' : item.kind.toUpperCase();
|
|
70
|
-
card.append(el('span', removed ? 'REMOVED' : kind, 'badge' + (removed ? ' neutral' : '')));
|
|
71
|
-
|
|
72
|
-
|
|
216
|
+
const heading = el('div', undefined, 'card-heading'); heading.append(el('span', removed ? 'REMOVED' : kind, 'badge' + (removed ? ' neutral' : '')), el('span', new Date(item.recorded_at).toLocaleDateString([], { month:'short', day:'numeric' }), 'record-date')); card.append(heading);
|
|
217
|
+
const copy = el('div', undefined, 'record-copy');
|
|
218
|
+
copy.append(el('h3', item.kind === 'answer' ? item.text : entryLabel(item)));
|
|
219
|
+
if (item.kind !== 'answer') copy.append(el('p', item.text, 'record-text'));
|
|
220
|
+
if (item.kind === 'next' && item.why) copy.append(el('p', item.why, 'record-text'));
|
|
221
|
+
if (item.answer) copy.append(el('p', item.answer, 'answer')); card.append(copy);
|
|
222
|
+
const compact = selected === 'overview' && !$('search').value.trim() && viewMode === 'records';
|
|
223
|
+
if (compact || item.text.length > 200 || item.answer?.length > 220 || item.kind === 'next' && item.why?.length > 220) {
|
|
224
|
+
card.className += ' has-preview';
|
|
225
|
+
let expanded = focusKey === 'record:' + item.id;
|
|
226
|
+
if (expanded) card.className += ' expanded';
|
|
227
|
+
const collapsedLabel = compact ? 'Details' : 'Read full entry';
|
|
228
|
+
const more = button(collapsedLabel, () => { expanded = !expanded; card.className = card.className.replace(' expanded', '') + (expanded ? ' expanded' : ''); more.textContent = expanded ? 'Show less' : collapsedLabel; more.setAttribute('aria-expanded', String(expanded)); }, 'read-more');
|
|
229
|
+
more.textContent = expanded ? 'Show less' : collapsedLabel;
|
|
230
|
+
more.setAttribute('aria-expanded', String(expanded)); card.append(more);
|
|
231
|
+
}
|
|
73
232
|
const actions = el('div', undefined, 'card-actions');
|
|
74
233
|
if (removed) actions.append(button('Restore to handoff', () => restore(item)));
|
|
75
234
|
else {
|
|
76
235
|
actions.append(button('Correct', () => edit(item)));
|
|
77
236
|
if (item.kind === 'next') actions.append(button(item.status === 'done' ? 'Reopen' : 'Mark done', () => changeStatus(item)));
|
|
237
|
+
if (viewMode === 'records') actions.append(button('Connections', () => { focusKey = 'record:' + item.id; setView('map'); renderMap(true); }));
|
|
78
238
|
actions.append(button('Remove from handoff', () => remove(item), 'remove'));
|
|
79
239
|
}
|
|
80
240
|
card.append(actions, metadata(item)); return card;
|
|
@@ -90,31 +250,57 @@ function checkCard(item, removed = false) {
|
|
|
90
250
|
card.append(metadata(item, true)); if (!removed) { const actions = el('div', undefined, 'card-actions'); actions.append(button('Remove from handoff', () => remove(item, 'check'), 'remove')); card.append(actions); }
|
|
91
251
|
return card;
|
|
92
252
|
}
|
|
93
|
-
function matches(item) { const query = $('search').value.toLowerCase().trim(); return !query ||
|
|
253
|
+
function matches(item) { const query = $('search').value.toLowerCase().trim(); return !query || searchableText(item).includes(query); }
|
|
94
254
|
function group(kind, items, limit = Infinity) {
|
|
95
255
|
const section = el('section', undefined, 'group'); const heading = el('div', undefined, 'group-title'); heading.append(el('h3', labels[kind]));
|
|
96
|
-
if (selected === 'overview') heading.append(button('View all →', () => { selected = kind; render(); $('section-title').focus(); }));
|
|
97
|
-
|
|
256
|
+
if (selected === 'overview') heading.append(button('View all →', () => { selected = kind; render(); $('section-title').focus(); }));
|
|
257
|
+
if (selected === 'overview') section.append(heading);
|
|
258
|
+
const cards = el('div', undefined, 'cards'); items.filter(matches).slice(0, limit).forEach(item => cards.append(kind === 'check' ? checkCard(item) : recordCard(item))); section.append(cards);
|
|
259
|
+
if (selected === 'overview' && !$('search').value.trim() && ['answer','decision'].includes(kind)) {
|
|
260
|
+
const archive = el('details', undefined, 'archive-group');
|
|
261
|
+
archive.append(el('summary', (kind === 'answer' ? 'Saved answers' : 'Recent decisions') + ' · ' + items.length), section);
|
|
262
|
+
return archive;
|
|
263
|
+
}
|
|
264
|
+
return section;
|
|
98
265
|
}
|
|
99
266
|
function render() {
|
|
100
267
|
if (!state) return;
|
|
101
268
|
renderAfterEditor = false; $('add').disabled = busy;
|
|
102
|
-
nav(); $('project').
|
|
269
|
+
nav(); summary(); $('project').replaceChildren(el('span', state.project_name, 'project-name'), el('span', state.branch || 'No Git branch', 'branch-name'));
|
|
103
270
|
$('revision').textContent = `Handoff revision ${state.revision}. Refreshed ${new Date().toLocaleTimeString()}.`;
|
|
104
271
|
$('section-title').textContent = labels[selected]; $('section-description').textContent = descriptions[selected];
|
|
105
272
|
$('goal').replaceChildren();
|
|
106
|
-
|
|
273
|
+
$('view-caption').textContent = viewMode === 'map' ? 'Connections' : 'Saved context';
|
|
274
|
+
$('summary').hidden = selected !== 'overview' || viewMode === 'map';
|
|
275
|
+
if (selected === 'overview' && viewMode === 'records') {
|
|
276
|
+
const goal = state.records.filter(r => r.kind === 'goal').sort((a,b) => b.revision - a.revision)[0];
|
|
277
|
+
if (goal) {
|
|
278
|
+
const node = el('details', undefined, 'goal');
|
|
279
|
+
node.append(el('summary', 'Current focus'), el('p', goal.text), button('Edit goal', () => edit(goal), 'quiet')); $('goal').append(node);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
107
282
|
const content = $('content'); content.replaceChildren();
|
|
283
|
+
content.dataset.compact = String(selected === 'overview' && !$('search').value.trim());
|
|
108
284
|
if (selected === 'removed') {
|
|
109
285
|
const cards = el('div', undefined, 'cards'); state.removed.filter(r => matches(r.item)).forEach(r => cards.append(r.category === 'check' ? checkCard(r.item, true) : recordCard(r.item, true))); content.append(cards);
|
|
110
286
|
} else if (selected === 'overview') {
|
|
111
|
-
for (const kind of ['next','answer','check','decision']) {
|
|
287
|
+
for (const kind of ['next','answer','check','decision','goal']) {
|
|
112
288
|
let items = [...(kind === 'check' ? state.checks : state.records.filter(r => r.kind === kind))].sort((a,b) => b.revision - a.revision);
|
|
113
|
-
if (kind === '
|
|
114
|
-
if (
|
|
289
|
+
if (kind === 'goal' && !$('search').value.trim()) continue;
|
|
290
|
+
if (kind === 'next' && !$('search').value.trim()) items = items.filter(item => item.status !== 'done');
|
|
291
|
+
if (kind === 'check' && !$('search').value.trim()) items = items.filter(item => item.freshness !== 'inputs-match');
|
|
292
|
+
if (kind === 'check') items.sort((a,b) => (a.freshness === 'inputs-match') - (b.freshness === 'inputs-match') || b.revision - a.revision);
|
|
293
|
+
if (items.some(matches)) content.append(group(kind, items, $('search').value.trim() || kind === 'next' || kind === 'check' ? Infinity : 2));
|
|
115
294
|
}
|
|
116
|
-
} else
|
|
295
|
+
} else {
|
|
296
|
+
const items = [...(selected === 'check' ? state.checks : state.records.filter(r => r.kind === selected))].sort((a,b) => b.revision - a.revision);
|
|
297
|
+
if (selected === 'next') items.sort((a,b) => (a.status === 'done') - (b.status === 'done') || b.revision - a.revision);
|
|
298
|
+
if (selected === 'check') items.sort((a,b) => (a.freshness === 'inputs-match') - (b.freshness === 'inputs-match') || b.revision - a.revision);
|
|
299
|
+
if (focusKey !== 'project') items.sort((a,b) => Number(focusKey.endsWith(':' + b.id)) - Number(focusKey.endsWith(':' + a.id)));
|
|
300
|
+
content.append(group(selected, items));
|
|
301
|
+
}
|
|
117
302
|
if (!content.querySelector('.card')) { const empty = el('div', undefined, 'empty'); empty.append(el('strong', $('search').value ? 'No matching memories' : selected === 'removed' ? 'Nothing removed' : 'A fresh start'), el('span', $('search').value ? 'Try a shorter search.' : selected === 'removed' ? 'Items you remove will appear here.' : 'Add an answer, a decision or the next step.')); content.replaceChildren(empty); }
|
|
303
|
+
renderMap();
|
|
118
304
|
}
|
|
119
305
|
function kindFields() {
|
|
120
306
|
$('answer-label').hidden = $('kind').value !== 'answer'; $('answer').required = $('kind').value === 'answer';
|
|
@@ -164,6 +350,9 @@ $('edit-form').addEventListener('submit', async event => {
|
|
|
164
350
|
try {
|
|
165
351
|
const kind = $('kind').value;
|
|
166
352
|
await action({ action:'save', branch:submitted.branch, id:submitted.id, expected_revision:submitted.item?.revision || 0, fields:{kind,text:$('text').value, ...(kind === 'answer' ? {answer:$('answer').value} : {}), ...($('why').value ? {why:$('why').value} : {}),status:kind === 'next' ? $('status').value : 'open'} });
|
|
353
|
+
// A successful save must be visible even if the old search or category
|
|
354
|
+
// would exclude it. Both views continue from the same saved entry.
|
|
355
|
+
focusKey = 'record:' + submitted.id; selected = kind; $('search').value = ''; render();
|
|
167
356
|
if (editing === submitted) $('editor').close();
|
|
168
357
|
notice(submitted.item ? 'Correction saved. The next session will use this version.' : 'Memory saved for the next session.');
|
|
169
358
|
} catch (error) { if (editing === submitted) { $('form-error').textContent = error.message; $('review-latest').hidden = !['refresh_required', 'save_unconfirmed'].includes(error.code); } }
|
|
@@ -194,12 +383,16 @@ $('keep-draft').addEventListener('click', () => {
|
|
|
194
383
|
$('editor').addEventListener('close', () => {
|
|
195
384
|
++stateRequest;
|
|
196
385
|
if (renderAfterEditor) render();
|
|
197
|
-
const
|
|
198
|
-
|
|
386
|
+
const activeWorkspace = $(viewMode === 'map' ? 'map-inspector' : 'content');
|
|
387
|
+
const card = [...activeWorkspace.querySelectorAll('[data-record-id]')].find(node => node.dataset.recordId === editing?.id);
|
|
388
|
+
(editorOpener?.isConnected ? editorOpener : card?.querySelector('.card-actions button') || (editing?.item?.kind === 'goal' && $('goal').querySelector('button')) || $('add')).focus();
|
|
199
389
|
editing = null; latestEdit = null;
|
|
200
390
|
});
|
|
201
391
|
$('editor').addEventListener('cancel', event => { if (editing?.saving) event.preventDefault(); });
|
|
202
392
|
$('kind').addEventListener('change', kindFields); $('cancel').addEventListener('click', () => $('editor').close()); $('cancel-top').addEventListener('click', () => $('editor').close());
|
|
203
|
-
$('add').addEventListener('click', () => edit()); $('refresh').addEventListener('click', refresh); $('search').addEventListener('input', render);
|
|
393
|
+
$('add').addEventListener('click', () => edit()); $('refresh').addEventListener('click', refresh); $('search').addEventListener('input', () => { focusKey = 'project'; render(); });
|
|
394
|
+
$('show-map').addEventListener('click',()=>setView('map'));$('show-records').addEventListener('click',()=>setView('records'));
|
|
395
|
+
$('map-suggestions').addEventListener('click',()=>{showSuggestions=!showSuggestions;renderMap();});
|
|
396
|
+
$('map-reset').addEventListener('click',()=>{focusKey='project';selected='overview';$('search').value='';render();});
|
|
204
397
|
document.addEventListener('visibilitychange', () => { if (!document.hidden && !$('editor').open && !busy) refresh(); });
|
|
205
398
|
refresh();
|
package/src/work/ui/index.html
CHANGED
|
@@ -2,24 +2,39 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Memoir · Project memory</title><link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script></head>
|
|
4
4
|
<body>
|
|
5
|
-
<a class="skip-link" href="#section-title">Skip to project memory</a>
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
<
|
|
5
|
+
<a id="skip-link" class="skip-link" href="#section-title">Skip to project memory</a>
|
|
6
|
+
<header class="workspace-bar">
|
|
7
|
+
<a class="workspace-brand" href="/" aria-label="Memoir project home"><span aria-hidden="true">m</span> memoir</a>
|
|
8
|
+
<span class="workspace-divider" aria-hidden="true">/</span><div id="project">Opening project…</div>
|
|
9
|
+
<span class="workspace-local"><i></i> On this computer</span><button id="refresh" class="quiet">Refresh</button>
|
|
10
|
+
</header>
|
|
11
|
+
<main class="workspace">
|
|
12
|
+
<header class="workspace-heading"><div><h1>Project memory</h1><p class="subtitle">Answers, decisions and checks for your next coding session.</p></div><button id="add" class="primary" disabled>+ Add context</button></header>
|
|
10
13
|
<div id="notice" role="status" aria-live="polite"></div>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
14
|
+
<div class="workspace-tools">
|
|
15
|
+
<label class="search-label"><span class="search-icon" aria-hidden="true"></span><span class="sr-only">Search saved project context</span><input id="search" type="search" placeholder="Search answers, decisions, files…" autocomplete="off"></label>
|
|
16
|
+
<div class="view-switch" aria-label="Project view"><button id="show-records" type="button" aria-pressed="true">Records</button><button id="show-map" type="button" aria-pressed="false">Map</button></div>
|
|
17
|
+
</div>
|
|
18
|
+
<nav id="navigation" aria-label="Memory categories"></nav>
|
|
19
|
+
<div class="review-summary"><section id="summary" class="summary-strip" aria-label="Project at a glance"></section><section id="goal" aria-label="Current goal"></section></div>
|
|
20
|
+
<div class="section-heading"><div><h2 id="section-title" tabindex="-1">Overview</h2><p id="section-description">What matters for your next session.</p></div><span id="view-caption">Saved context</span></div>
|
|
21
|
+
<section id="records-workspace" aria-label="Project records"><div id="content" aria-live="polite"></div></section>
|
|
22
|
+
<section id="map-workspace" hidden aria-label="Project knowledge map">
|
|
23
|
+
<div class="map-controls"><p>Choose an entry to follow its connections.</p><button id="map-suggestions" type="button" aria-pressed="false">Suggested links</button></div>
|
|
24
|
+
<div class="map-layout">
|
|
25
|
+
<div class="map-surface"><div class="canvas-caption"><span id="map-count">Opening project memory</span><button id="map-reset" type="button">Back to overview</button></div><div id="map-canvas" class="map-canvas" aria-label="Connected project entries"></div><div class="map-bottom"><div class="map-legend"><span><i class="line-solid"></i> Recorded</span><span id="map-suggested-legend" hidden><i class="line-dashed"></i> Suggested word match</span></div></div></div>
|
|
26
|
+
<aside id="map-inspector" class="map-inspector" aria-live="polite" aria-label="Selected context"></aside>
|
|
27
|
+
</div>
|
|
28
|
+
<p class="map-footnote">Recorded links show project membership, named records or covered files. Suggested links are optional word matches.</p>
|
|
29
|
+
</section>
|
|
30
|
+
<footer><span id="revision"></span><details><summary>Used by your coding tools</summary><p>Memoir’s CLI, connected coding tools and this view read the same local project record. Saved corrections are available the next time a tool reads it. You can close this page and keep working.</p><p>Personal memory is separate. Check results cover their listed files; changed files may need a new check. A saved record is never permission to run a command or publish.</p></details></footer>
|
|
16
31
|
</main>
|
|
17
32
|
<dialog id="editor" aria-labelledby="editor-title"><form id="edit-form">
|
|
18
33
|
<div class="dialog-heading"><h2 id="editor-title">Correct memory</h2><button type="button" id="cancel-top" class="quiet" aria-label="Close editor">×</button></div>
|
|
19
34
|
<label>Type<select id="kind"><option value="answer">Answered question</option><option value="decision">Decision</option><option value="next">Next step</option><option value="goal">Goal</option></select></label>
|
|
20
35
|
<label><span id="text-label">Question</span><textarea id="text" rows="3" maxlength="2000" required></textarea></label>
|
|
21
36
|
<label id="answer-label">Answer<textarea id="answer" rows="3" maxlength="2000"></textarea></label>
|
|
22
|
-
<label>Why this matters <span class="optional">(optional)</span><textarea id="why" rows="2" maxlength="2000"></textarea></label>
|
|
37
|
+
<label><span>Why this matters <span class="optional">(optional)</span></span><textarea id="why" rows="2" maxlength="2000"></textarea></label>
|
|
23
38
|
<label id="status-label">Progress<select id="status"><option value="open">To do</option><option value="done">Done</option></select></label>
|
|
24
39
|
<p class="hint">Only save project context. Keep credentials and personal details out. Corrections keep earlier versions.</p>
|
|
25
40
|
<p id="form-error" class="form-error" role="alert"></p>
|
package/src/work/ui/style.css
CHANGED
|
@@ -1,3 +1,248 @@
|
|
|
1
|
-
:root
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--ink: #eceeea;
|
|
4
|
+
--muted: #a0a59f;
|
|
5
|
+
--paper: #151816;
|
|
6
|
+
--panel: #1b1e1b;
|
|
7
|
+
--line: #343a34;
|
|
8
|
+
--accent: #c6dda5;
|
|
9
|
+
--soft: #2d3828;
|
|
10
|
+
--warn: #e7c48d;
|
|
11
|
+
--warn-bg: #382f20;
|
|
12
|
+
font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body { margin: 0; background: var(--paper); color: var(--ink); }
|
|
16
|
+
button, input, select, textarea { font: inherit; }
|
|
17
|
+
button { cursor: pointer; border: 1px solid transparent; border-radius: 6px; padding: 8px 12px; font-weight: 500; color: inherit; background: var(--panel); transition: background .15s, border-color .15s; }
|
|
18
|
+
button:hover { background: #30372e; border-color: #59644f; }
|
|
19
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
20
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
|
21
|
+
[hidden] { display: none !important; }
|
|
22
|
+
.workspace-bar { min-height: 68px; padding: 12px 32px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 20px; }
|
|
23
|
+
.workspace-brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-size: 22px; letter-spacing: -.8px; font-weight: 600; }
|
|
24
|
+
.workspace-brand > span { color: var(--accent); font: 700 28px/1 Georgia,serif; border: 1px solid #617050; border-radius: 6px 6px 2px 2px; width: 30px; height: 31px; text-align: center; padding-bottom: 3px; }
|
|
25
|
+
.workspace-divider { color: #737a6d; font-size: 21px; font-weight: 300; }
|
|
26
|
+
#project { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
27
|
+
.project-name { font-size: 13px; font-weight: 550; }
|
|
28
|
+
.branch-name { color: var(--muted); font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; border: 1px solid var(--line); padding: 3px 8px; border-radius: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 230px; }
|
|
29
|
+
.workspace-local { margin-left: auto; display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 11px; white-space: nowrap; }
|
|
30
|
+
.workspace-local i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
|
|
31
|
+
.quiet { background: transparent; border-color: var(--line); font-size: 12px; }
|
|
32
|
+
#refresh { margin-left: auto; }
|
|
33
|
+
.workspace-local + #refresh { margin-left: 0; }
|
|
34
|
+
.workspace { max-width: 1380px; margin: auto; padding: 28px 32px 24px; }
|
|
35
|
+
.workspace-heading { display: flex; align-items: center; justify-content: space-between; gap: 28px; margin-bottom: 30px; }
|
|
36
|
+
.eyebrow { margin: 0 0 10px; color: #b4c49e; font-size: 10px; font-weight: 550; letter-spacing: 1.6px; }
|
|
37
|
+
h1 { font-size: clamp(29px, 3vw, 38px); line-height: 1.15; letter-spacing: -1.2px; margin: 0 0 12px; font-weight: 550; }
|
|
38
|
+
.subtitle { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.7; }
|
|
39
|
+
.primary, .map-add { background: var(--accent); color: #20271a; border-color: var(--accent); font-size: 12px; padding: 10px 16px; white-space: nowrap; }
|
|
40
|
+
.primary:hover, .map-add:hover { background: #d5e9b9; border-color: #d5e9b9; }
|
|
41
|
+
.workspace-tools { display: flex; align-items: center; gap: 18px; margin-bottom: 18px; }
|
|
42
|
+
.search-label { position: relative; display: block; flex: 1; max-width: 470px; }
|
|
43
|
+
.search-icon { position: absolute; top: 15px; left: 15px; width: 12px; height: 12px; border: 1.5px solid var(--muted); border-radius: 50%; }
|
|
44
|
+
.search-icon::after { content: ''; position: absolute; width: 5px; height: 1.5px; background: var(--muted); transform: rotate(45deg); right: -4px; bottom: -2px; }
|
|
45
|
+
input[type=search] { background: var(--panel); border: 1px solid var(--line); border-radius: 7px; padding: 11px 15px 11px 39px; width: 100%; font-size: 12px; color: var(--ink); }
|
|
46
|
+
input::placeholder { color: #9ba296; }
|
|
47
|
+
.view-switch { display: flex; gap: 3px; padding: 4px; background: #101310; border: 1px solid var(--line); border-radius: 7px; margin-left: auto; }
|
|
48
|
+
.view-switch button { color: var(--muted); background: transparent; font-size: 12px; padding: 6px 17px; border-radius: 4px; border: 0; }
|
|
49
|
+
.view-switch button[aria-pressed=true] { color: var(--ink); background: #343c2f; }
|
|
50
|
+
nav { display: flex; align-items: center; gap: 20px; border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: thin; scrollbar-color: #535d49 transparent; }
|
|
51
|
+
nav button { display: flex; align-items: center; gap: 8px; background: transparent; color: var(--muted); padding: 10px 1px 15px; font-size: 12px; border: 0; border-radius: 0; white-space: nowrap; }
|
|
52
|
+
nav button:hover { background: transparent; color: var(--ink); }
|
|
53
|
+
nav button[aria-current=page] { color: var(--accent); box-shadow: inset 0 -2px var(--accent); }
|
|
54
|
+
.nav-icon { display: none; }
|
|
55
|
+
nav .count { color: #b6bcae; font-size: 10px; background: #2b3028; padding: 0 5px; border-radius: 4px; font-variant-numeric: tabular-nums; }
|
|
56
|
+
.review-summary { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 0 24px; }
|
|
57
|
+
.summary-strip { display: flex; flex-wrap: wrap; gap: 8px 24px; padding: 16px 0; flex: 1; }
|
|
58
|
+
#goal { max-width: 440px; }
|
|
59
|
+
.summary-item { display: flex; align-items: center; gap: 7px; padding: 0; background: transparent; border: 0; font-size: 12px; color: var(--muted); font-weight: 400; }
|
|
60
|
+
.summary-item:hover { background: transparent; color: var(--ink); }
|
|
61
|
+
.summary-item strong { color: var(--ink); font-size: 13px; font-weight: 550; font-variant-numeric: tabular-nums; }
|
|
62
|
+
.summary-item.needs-attention strong { color: var(--warn); }
|
|
63
|
+
.summary-arrow { font-size: 11px; color: #a9b19c; }
|
|
64
|
+
.goal { padding: 16px 0; }
|
|
65
|
+
.goal summary { cursor: pointer; color: #c8d3b9; font-size: 12px; }
|
|
66
|
+
.goal p { margin: 12px 0; font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; }
|
|
67
|
+
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin: 18px 0; }
|
|
68
|
+
h2 { font-size: 19px; letter-spacing: -.4px; margin: 0; font-weight: 550; }
|
|
69
|
+
.section-heading p { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
|
|
70
|
+
#view-caption { font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
71
|
+
.group { margin-bottom: 26px; }
|
|
72
|
+
.group-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px; }
|
|
73
|
+
.group-title h3 { font-size: 12px; font-weight: 500; margin: 0; color: #bcc7ae; }
|
|
74
|
+
.group-title button { font-size: 11px; padding: 3px 0 3px 10px; background: transparent; color: var(--muted); border: 0; }
|
|
75
|
+
.cards { display: grid; grid-template-columns: 1fr; gap: 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: var(--panel); }
|
|
76
|
+
.card { padding: 20px 24px 17px; min-width: 0; }
|
|
77
|
+
.cards > .card + .card { border-top: 1px solid var(--line); }
|
|
78
|
+
.card-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 7px; }
|
|
79
|
+
.record-date { font-size: 11px; color: var(--muted); margin-left: auto; }
|
|
80
|
+
.card h3 { font-size: 14px; margin: 5px 0 8px; font-weight: 550; line-height: 1.6; overflow-wrap: anywhere; color: var(--ink); }
|
|
81
|
+
.card p { margin: 8px 0; overflow-wrap: anywhere; }
|
|
82
|
+
.record-text, .card .answer { font-size: 13px; line-height: 1.75; color: #bdc4b6; white-space: pre-wrap; max-width: 100ch; }
|
|
83
|
+
.has-preview:not(.expanded) .record-copy p, .has-preview:not(.expanded) .record-copy h3 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
|
|
84
|
+
.read-more { color: #c4d3ad; font-size: 11px; padding: 2px 0; background: transparent; border: 0; }
|
|
85
|
+
.read-more:hover { background: transparent; text-decoration: underline; }
|
|
86
|
+
.badge { display: inline-flex; align-items: center; padding: 2px 6px; border-radius: 3px; background: #2c3626; color: #c5d6b0; font-size: 9px; letter-spacing: .5px; font-weight: 550; }
|
|
87
|
+
.badge.warn { background: var(--warn-bg); color: var(--warn); }
|
|
88
|
+
.badge.neutral, .done .badge { background: #30332e; color: var(--muted); }
|
|
89
|
+
.card-actions { display: flex; align-items: center; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
|
|
90
|
+
.card-actions button { font-size: 11px; padding: 2px 0; background: transparent; border: 0; border-radius: 2px; color: #c0c9b5; }
|
|
91
|
+
.card-actions button:hover { color: var(--accent); text-decoration: underline; }
|
|
92
|
+
.card-actions .remove { margin-left: auto; color: #a0a69a; }
|
|
93
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 2px 18px; padding: 15px 20px; }
|
|
94
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .card-heading { margin: 0; }
|
|
95
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .record-date { display: none; }
|
|
96
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .record-copy { grid-column: 1; }
|
|
97
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .record-copy h3 { margin: 4px 0 0; }
|
|
98
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .record-copy p,
|
|
99
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .card-actions,
|
|
100
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .metadata { display: none; }
|
|
101
|
+
#content[data-compact=true] .card.has-preview:not(.expanded) .read-more { grid-column: 2; grid-row: 1 / 3; align-self: center; border: 1px solid var(--line); padding: 6px 12px; }
|
|
102
|
+
.archive-group { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 14px; }
|
|
103
|
+
.archive-group > summary { cursor: pointer; color: #b8c1ad; font-size: 12px; }
|
|
104
|
+
.archive-group > .group { margin-top: 16px; }
|
|
105
|
+
.metadata { font-size: 11px; color: var(--muted); margin-top: 12px; }
|
|
106
|
+
.metadata p { white-space: pre-wrap; line-height: 1.8; }
|
|
107
|
+
.metadata summary { cursor: pointer; color: #a8b29d; }
|
|
108
|
+
.metadata[open] summary { color: var(--accent); }
|
|
109
|
+
.metadata ul { padding-left: 19px; }
|
|
110
|
+
.metadata code { font-size: 10px; word-break: break-all; }
|
|
111
|
+
.metadata li { overflow-wrap: anywhere; }
|
|
112
|
+
.reasons { border-left: 2px solid #b29768; padding-left: 11px; color: var(--warn); font-size: 12px; list-style: none; overflow-wrap: anywhere; }
|
|
113
|
+
.hidden-note { color: var(--muted); font-size: 12px; line-height: 1.7; }
|
|
114
|
+
.done .record-copy p { color: var(--muted); }
|
|
115
|
+
.empty { padding: 45px 25px; border: 1px solid var(--line); background: var(--panel); border-radius: 9px; color: var(--muted); text-align: center; font-size: 13px; }
|
|
116
|
+
.empty strong { display: block; color: var(--ink); margin-bottom: 6px; font-size: 16px; font-weight: 500; }
|
|
117
|
+
.notice { padding: 12px 16px; color: #d3e4c8; background: #253320; border: 1px solid #46583b; border-radius: 7px; font-size: 12px; margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
118
|
+
.notice.error { background: #38271f; color: #f0cab4; border-color: #785641; }
|
|
119
|
+
.notice button { font-size: 12px; background: transparent; text-decoration: underline; }
|
|
120
|
+
footer { margin-top: 28px; border-top: 1px solid var(--line); padding-top: 18px; color: var(--muted); font-size: 11px; display: flex; justify-content: space-between; align-items: flex-start; gap: 22px; flex-wrap: wrap; }
|
|
121
|
+
footer details { max-width: 550px; }
|
|
122
|
+
footer summary { cursor: pointer; }
|
|
123
|
+
footer p { line-height: 1.8; }
|
|
124
|
+
.map-controls { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
|
125
|
+
.map-controls p { margin: 0; font-size: 12px; color: var(--muted); }
|
|
126
|
+
#map-suggestions { padding: 6px 10px; background: transparent; border-color: var(--line); color: #c5cdba; font-size: 11px; }
|
|
127
|
+
#map-suggestions[aria-pressed=true] { color: var(--accent); border-color: #81946b; background: var(--soft); }
|
|
128
|
+
.map-layout { display: grid; grid-template-columns: minmax(0,1fr) 320px; gap: 18px; align-items: start; }
|
|
129
|
+
.map-surface { position: relative; min-width: 0; background: var(--panel); border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
|
|
130
|
+
.canvas-caption { position: relative; z-index: 1; padding: 17px 20px 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 11px; }
|
|
131
|
+
.canvas-caption button { background: transparent; color: #c2cbb6; font-size: 11px; border: 0; padding: 3px 0; }
|
|
132
|
+
.map-canvas { position: relative; height: 440px; isolation: isolate; background-image: radial-gradient(#87937b25 .7px,transparent .7px); background-size: 22px 22px; }
|
|
133
|
+
.map-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
|
|
134
|
+
.map-line { stroke: #7b896e; stroke-width: 1; fill: none; vector-effect: non-scaling-stroke; opacity: .4; }
|
|
135
|
+
.map-line.suggested { stroke: #b3a6c6; stroke-dasharray: 3 6; opacity: .5; }
|
|
136
|
+
.map-line.focused { stroke: #bed49d; stroke-width: 1.5; opacity: .9; }
|
|
137
|
+
.map-node { position: absolute; z-index: 1; transform: translate(-50%,-12px); width: 128px; padding: 0 5px 5px; border: 0; background: transparent; text-align: center; border-radius: 7px; color: #d6decf; --node-color: #b9a8d2; }
|
|
138
|
+
.map-node:hover { background: transparent; color: white; }
|
|
139
|
+
.map-node[data-kind=next], .index-entry[data-kind=next] { --node-color: #cbd69d; }
|
|
140
|
+
.map-node[data-kind=answer], .index-entry[data-kind=answer] { --node-color: #9ccbb0; }
|
|
141
|
+
.map-node[data-kind=check], .index-entry[data-kind=check] { --node-color: #9ebfd7; }
|
|
142
|
+
.map-node[data-kind=goal], .index-entry[data-kind=goal] { --node-color: #d7b397; }
|
|
143
|
+
.map-node[data-kind=project] { --node-color: var(--accent); transform: translate(-50%,-28px); width: 135px; }
|
|
144
|
+
.node-dot { display: block; width: 10px; height: 10px; border-radius: 50%; margin: 5px auto 10px; background: var(--node-color); box-shadow: 0 0 0 5px var(--panel),0 0 0 6px #6c76534d; }
|
|
145
|
+
.node-label { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; line-height: 1.5; font-size: 12px; font-weight: 500; overflow-wrap: anywhere; background: var(--panel); padding: 1px 3px; }
|
|
146
|
+
.node-kind { display: block; margin-top: 3px; font-size: 10px; color: #a7b198; font-weight: 400; }
|
|
147
|
+
.map-node.selected:not([data-kind=project]) .node-dot { box-shadow: 0 0 0 5px var(--panel),0 0 0 6px var(--node-color); }
|
|
148
|
+
.map-node[data-kind=project] .node-dot { width: 39px; height: 39px; margin-bottom: 12px; background: #303b27; border: 1px solid #9ab584; }
|
|
149
|
+
.map-node[data-kind=project] .node-dot::after { content: 'm'; color: var(--accent); font: 700 31px/32px Georgia,serif; }
|
|
150
|
+
.map-node[data-kind=project] .node-label { font-size: 15px; font-weight: 600; }
|
|
151
|
+
.map-node[data-center=true]:not([data-kind=project]) .node-dot { width: 16px; height: 16px; margin-top: 2px; }
|
|
152
|
+
.map-bottom { padding: 13px 20px 17px; }
|
|
153
|
+
.map-legend { display: flex; flex-wrap: wrap; gap: 18px; color: var(--muted); font-size: 10px; }
|
|
154
|
+
.map-legend span { display: inline-flex; align-items: center; gap: 7px; }
|
|
155
|
+
.line-solid, .line-dashed { width: 19px; border-top: 1px solid #909e80; }
|
|
156
|
+
.line-dashed { border-top: 1px dashed #b6a5c7; }
|
|
157
|
+
.map-empty { position: absolute; left: 12px; right: 12px; bottom: 25px; text-align: center; font-size: 12px; color: var(--muted); }
|
|
158
|
+
.map-inspector { min-width: 0; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); padding: 22px; max-height: 533px; overflow: auto; scrollbar-color: #56624b var(--panel); }
|
|
159
|
+
.inspector-eyebrow { color: #b5c2a4; font-size: 9px; letter-spacing: 1.4px; margin: 0 0 9px; }
|
|
160
|
+
.map-inspector > h2 { font-size: 21px; font-weight: 500; margin: 0 0 16px; line-height: 1.4; overflow-wrap: anywhere; }
|
|
161
|
+
.project-brief { color: #c4ceba; font-size: 13px; line-height: 1.8; margin: 0 0 16px; }
|
|
162
|
+
.inspector-hint { color: var(--muted); font-size: 12px; line-height: 1.8; margin: 0 0 20px; }
|
|
163
|
+
.map-add { width: 100%; }
|
|
164
|
+
.inspector-index, .inspector-connections { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
|
|
165
|
+
.inspector-index h3, .inspector-connections h3 { font-size: 12px; font-weight: 500; color: #c2cdb7; margin: 0 0 12px; }
|
|
166
|
+
.index-entry { --node-color: #b9a8d2; display: flex; align-items: center; gap: 9px; background: transparent; color: #c2cdb7; border: 0; padding: 10px 3px; width: 100%; text-align: left; font-size: 12px; font-weight: 400; }
|
|
167
|
+
.index-entry > i { width: 5px; height: 5px; flex-shrink: 0; border-radius: 50%; background: var(--node-color); }
|
|
168
|
+
.index-entry > span:nth-child(2) { flex: 1; }
|
|
169
|
+
.index-entry:hover { background: #2a3325; }
|
|
170
|
+
.inspector-back { display: block; background: transparent; color: var(--accent); font-size: 11px; padding: 10px 0 17px; border: 0; }
|
|
171
|
+
.inspector-back:hover { background: transparent; text-decoration: underline; }
|
|
172
|
+
.map-inspector .card { padding: 0; margin: 0; }
|
|
173
|
+
.map-inspector .record-copy .record-text, .map-inspector .record-copy .answer { display: block; }
|
|
174
|
+
.map-inspector .card .read-more { display: none; }
|
|
175
|
+
.map-inspector .card-actions .remove { margin-left: 0; }
|
|
176
|
+
.connection-row { display: grid; text-align: left; gap: 6px; padding: 12px; background: #232a20; border: 1px solid #3a4532; color: #c8d5ba; width: 100%; margin-bottom: 9px; border-radius: 6px; }
|
|
177
|
+
.connection-type { font-size: 9px; letter-spacing: .7px; color: #c1d6a5; font-weight: 500; }
|
|
178
|
+
.connection-type.suggested { color: #c5b3d8; }
|
|
179
|
+
.connection-row strong { font-size: 12px; font-weight: 500; }
|
|
180
|
+
.connection-reason, .connection-note { font-size: 11px; color: #aebc9e; font-weight: 400; line-height: 1.8; overflow-wrap: anywhere; }
|
|
181
|
+
.map-footnote { color: var(--muted); font-size: 11px; margin: 16px 0 0; line-height: 1.8; }
|
|
182
|
+
dialog { border: 1px solid #545f48; border-radius: 12px; padding: 26px; width: min(560px, calc(100% - 32px)); max-height: 90vh; color: var(--ink); background: var(--panel); box-shadow: 0 22px 90px #0008; }
|
|
183
|
+
dialog::backdrop { background: #050805b0; backdrop-filter: blur(3px); }
|
|
184
|
+
dialog form { display: grid; gap: 15px; }
|
|
185
|
+
.dialog-heading, .dialog-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
186
|
+
.dialog-heading { padding-bottom: 8px; }
|
|
187
|
+
.dialog-heading h2 { font-size: 21px; }
|
|
188
|
+
.dialog-heading button { font-size: 22px; padding: 0 10px; border: 0; }
|
|
189
|
+
.dialog-actions { justify-content: flex-end; padding-top: 14px; border-top: 1px solid var(--line); }
|
|
190
|
+
label { display: grid; gap: 6px; font-size: 12px; font-weight: 500; }
|
|
191
|
+
textarea, select { width: 100%; border: 1px solid #4b5640; border-radius: 6px; padding: 9px 11px; font-size: 13px; color: var(--ink); background: #151a12; line-height: 1.7; }
|
|
192
|
+
textarea { resize: vertical; }
|
|
193
|
+
.hint, .optional { font-size: 11px; font-weight: 400; color: var(--muted); }
|
|
194
|
+
.hint { margin: 0; line-height: 1.7; }
|
|
195
|
+
.form-error { color: #f0cab4; font-size: 12px; margin: 0; }
|
|
196
|
+
.comparison { padding: 14px; background: var(--soft); border-radius: 7px; font-size: 12px; }
|
|
197
|
+
.comparison h3 { margin: 0 0 8px; font-size: 13px; }
|
|
198
|
+
.comparison pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
|
|
199
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
200
|
+
.skip-link { position: fixed; top: -100px; left: 12px; z-index: 10; padding: 12px; background: var(--accent); color: #20271a; }
|
|
201
|
+
.skip-link:focus { top: 12px; }
|
|
202
|
+
@media (max-width: 1050px) { .map-layout { grid-template-columns: minmax(0,1fr) 280px; gap: 14px; } .map-node { width: 108px; } }
|
|
203
|
+
@media (max-width: 950px) {
|
|
204
|
+
.workspace-bar { padding: 12px 24px; gap: 15px; }
|
|
205
|
+
.workspace-local { display: none; }
|
|
206
|
+
.workspace-local + #refresh { margin-left: auto; }
|
|
207
|
+
.workspace { padding: 28px 24px 22px; }
|
|
208
|
+
.workspace-heading { margin-bottom: 24px; }
|
|
209
|
+
.branch-name { max-width: 190px; }
|
|
210
|
+
nav { gap: 19px; }
|
|
211
|
+
.map-layout { grid-template-columns: 1fr; }
|
|
212
|
+
.map-node { width: 128px; }
|
|
213
|
+
.map-inspector { max-height: none; }
|
|
214
|
+
}
|
|
215
|
+
@media (max-width: 560px) {
|
|
216
|
+
.workspace-bar { padding: 12px 17px; gap: 12px; min-height: 62px; }
|
|
217
|
+
.workspace-brand { font-size: 20px; }
|
|
218
|
+
.branch-name { display: none; }
|
|
219
|
+
.project-name { font-size: 12px; }
|
|
220
|
+
.workspace { padding: 25px 17px 20px; }
|
|
221
|
+
.workspace-heading { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
|
|
222
|
+
.workspace-heading .primary { padding: 8px 12px; }
|
|
223
|
+
.eyebrow { font-size: 9px; }
|
|
224
|
+
h1 { font-size: 30px; }
|
|
225
|
+
.subtitle { font-size: 12px; }
|
|
226
|
+
.workspace-tools { gap: 10px; }
|
|
227
|
+
.view-switch button { padding: 6px 10px; font-size: 11px; }
|
|
228
|
+
input[type=search] { padding-left: 32px; font-size: 11px; }
|
|
229
|
+
.search-icon { left: 12px; }
|
|
230
|
+
nav { gap: 18px; }
|
|
231
|
+
nav button { font-size: 11px; }
|
|
232
|
+
.summary-strip { gap: 12px 18px; }
|
|
233
|
+
.summary-item { font-size: 11px; }
|
|
234
|
+
.section-heading { margin: 24px 0 18px; }
|
|
235
|
+
#view-caption { display: none; }
|
|
236
|
+
.card { padding: 17px; }
|
|
237
|
+
.card-actions { gap: 14px; }
|
|
238
|
+
.card-actions .remove { margin-left: 0; }
|
|
239
|
+
.map-node { width: min(96px,24vw); }
|
|
240
|
+
.node-label { font-size: 11px; }
|
|
241
|
+
.node-kind { font-size: 9px; }
|
|
242
|
+
.map-node[data-kind=project] { width: 98px; }
|
|
243
|
+
.canvas-caption { font-size: 10px; padding: 13px 12px 0; }
|
|
244
|
+
.canvas-caption button { font-size: 10px; }
|
|
245
|
+
.map-bottom { padding: 13px 12px; }
|
|
246
|
+
.map-inspector { padding: 20px; }
|
|
247
|
+
}
|
|
248
|
+
@media (prefers-reduced-motion: reduce) { button { transition: none; } }
|