@unson/brainbase-mcp 0.41.13 → 0.43.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/dist/local-web-host.d.ts +6 -0
- package/dist/local-web-host.js +35 -1
- package/dist/personal-memory.d.ts +101 -0
- package/dist/personal-memory.js +204 -0
- package/dist/ssot.d.ts +3 -1
- package/dist/ssot.js +4 -0
- package/package.json +11 -2
- package/ui/area-nav.css +56 -4
- package/ui/area-nav.js +130 -10
- package/ui/inbox.css +189 -0
- package/ui/inbox.js +578 -0
- package/ui/judgment-history.js +74 -3
- package/ui/local-web-shell.css +4 -2
- package/ui/local-web-shell.js +107 -51
- package/ui/personal-memory-http-port.js +72 -0
- package/ui/personal-memory.css +254 -0
- package/ui/personal-memory.js +677 -0
package/ui/area-nav.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Area navigation shared by OSS Personal and the organization edition
|
|
3
|
-
* (brainbase-project ledger D10
|
|
3
|
+
* (brainbase-project ledger D10).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Each line on the left is an area: usually one screen (a destination itself,
|
|
6
|
+
* 2026-10-17 revision), or several screens shown as tabs above the page (設定).
|
|
7
|
+
* An area link opens the screen last used in that area, which is remembered in
|
|
8
|
+
* the viewer's browser only (a display preference, not a fact of the
|
|
9
|
+
* organization). Optional `sections` group the lines under small headings and
|
|
10
|
+
* put some at the bottom; a heading can open a screen of its own, and the
|
|
11
|
+
* caller can put links at the top of a section (starred projects). Without
|
|
12
|
+
* `sections` the lines sit directly in the nav, as in stage 2. Every link is a
|
|
13
|
+
* link to a screen, so screen addresses never change. The part draws links
|
|
14
|
+
* only: showing and loading screens stays with the caller (`show` in,
|
|
15
|
+
* `onSelect` / `onSelectLink` out).
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
export const AREA_NAV_CONTRACT_VERSION = 'brainbase.area-nav.v1';
|
|
@@ -78,12 +83,18 @@ export function resolveAreaTarget(area, { remembered = null, isShown = () => tru
|
|
|
78
83
|
/**
|
|
79
84
|
* @param {object} options
|
|
80
85
|
* @param {Document} options.document
|
|
81
|
-
* @param {Array<{id: string, label: string|null, defaultScreen?: string, screenIds: string[]}>} options.areas
|
|
86
|
+
* @param {Array<{id: string, label: string|null, defaultScreen?: string, screenIds: string[], section?: string}>} options.areas
|
|
82
87
|
* In menu order. A null label takes the label of the area's only screen.
|
|
83
88
|
* @param {Array<{id: string, label: string, href?: string, external?: boolean, hidden?: boolean}>} options.screens
|
|
89
|
+
* @param {Array<{id: string, label: string|null, placement?: 'main'|'footer', screenId?: string}>} [options.sections]
|
|
90
|
+
* Groups of lines. A section sits where its first area is (a section without
|
|
91
|
+
* areas after the other main ones); `footer` sections come last. `screenId`
|
|
92
|
+
* makes the heading a link to that screen. Areas without a section are grouped
|
|
93
|
+
* with their neighbours, without a heading.
|
|
84
94
|
* @param {{get(areaId: string): string|null, set(areaId: string, screenId: string): void}} [options.memory]
|
|
85
95
|
* @param {(screenId: string) => string} [options.hrefFor] Address of a screen (default `#<id>`).
|
|
86
96
|
* @param {(screenId: string, event: Event) => void} [options.onSelect]
|
|
97
|
+
* @param {(linkId: string, event: Event) => void} [options.onSelectLink] A link given by `setLinks`.
|
|
87
98
|
* @param {(area: object) => Node|null} [options.iconFor] Icon in front of an area link.
|
|
88
99
|
* @param {string} [options.linkClassName] Extra class for the area links (the host's look).
|
|
89
100
|
*/
|
|
@@ -91,9 +102,11 @@ export function createAreaNav({
|
|
|
91
102
|
document: doc,
|
|
92
103
|
areas: areaInput,
|
|
93
104
|
screens: screenInput,
|
|
105
|
+
sections: sectionInput,
|
|
94
106
|
memory = createAreaMemory(null),
|
|
95
107
|
hrefFor = (screenId) => `#${screenId}`,
|
|
96
108
|
onSelect,
|
|
109
|
+
onSelectLink,
|
|
97
110
|
iconFor,
|
|
98
111
|
linkClassName = '',
|
|
99
112
|
label = '画面',
|
|
@@ -108,10 +121,18 @@ export function createAreaNav({
|
|
|
108
121
|
count: null,
|
|
109
122
|
waiting: false,
|
|
110
123
|
}]));
|
|
124
|
+
const sections = new Map((sectionInput ?? []).map((section) => [section.id, {
|
|
125
|
+
id: section.id,
|
|
126
|
+
label: section.label ?? null,
|
|
127
|
+
footer: section.placement === 'footer',
|
|
128
|
+
screenId: section.screenId ?? null,
|
|
129
|
+
links: [],
|
|
130
|
+
}]));
|
|
111
131
|
const areas = (areaInput ?? []).map((area) => ({
|
|
112
132
|
id: area.id,
|
|
113
133
|
label: area.label ?? null,
|
|
114
134
|
defaultScreen: area.defaultScreen ?? null,
|
|
135
|
+
section: sections.has(area.section) ? area.section : null,
|
|
115
136
|
screens: area.screenIds.map((id) => screens.get(id)).filter(Boolean),
|
|
116
137
|
})).filter((area) => area.screens.length > 0);
|
|
117
138
|
const areaByScreen = new Map();
|
|
@@ -137,10 +158,11 @@ export function createAreaNav({
|
|
|
137
158
|
const tabs = makeElement(doc, 'nav', { className: 'bb-area-tabs' });
|
|
138
159
|
tabs.hidden = true;
|
|
139
160
|
const areaLinks = new Map();
|
|
161
|
+
const linkClass = ['bb-area-link', linkClassName].filter(Boolean).join(' ');
|
|
140
162
|
|
|
141
163
|
for (const area of areas) {
|
|
142
164
|
const link = makeElement(doc, 'a', {
|
|
143
|
-
className:
|
|
165
|
+
className: linkClass,
|
|
144
166
|
attrs: { 'data-area': area.id },
|
|
145
167
|
});
|
|
146
168
|
link.addEventListener('click', (event) => {
|
|
@@ -148,7 +170,87 @@ export function createAreaNav({
|
|
|
148
170
|
if (target) onSelect?.(target, event);
|
|
149
171
|
});
|
|
150
172
|
areaLinks.set(area.id, link);
|
|
151
|
-
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Groups of lines: one per section, plus runs of areas without a section.
|
|
176
|
+
const groups = [];
|
|
177
|
+
if (sections.size === 0) {
|
|
178
|
+
nav.append(...areas.map((area) => areaLinks.get(area.id)));
|
|
179
|
+
} else {
|
|
180
|
+
const groupOf = new Map();
|
|
181
|
+
const makeGroup = (section) => {
|
|
182
|
+
const element = makeElement(doc, 'div', {
|
|
183
|
+
className: section?.footer ? 'bb-area-section is-footer' : 'bb-area-section',
|
|
184
|
+
attrs: {
|
|
185
|
+
role: 'group',
|
|
186
|
+
'aria-label': section?.label ?? null,
|
|
187
|
+
'data-section': section?.id ?? null,
|
|
188
|
+
'data-placement': section?.footer ? 'footer' : null,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const group = { section, element, headingText: null, headingLink: null, linkSlot: null, areas: [] };
|
|
192
|
+
if (section) {
|
|
193
|
+
// Both forms of the heading are made once; render shows one of them.
|
|
194
|
+
group.headingText = makeElement(doc, 'div', { className: 'bb-area-section-label', text: section.label ?? '' });
|
|
195
|
+
group.headingLink = makeElement(doc, 'a', { className: 'bb-area-section-label bb-area-section-link', text: section.label ?? '' });
|
|
196
|
+
group.headingLink.addEventListener('click', (event) => {
|
|
197
|
+
if (section.screenId) onSelect?.(section.screenId, event);
|
|
198
|
+
});
|
|
199
|
+
group.linkSlot = makeElement(doc, 'div', { className: 'bb-area-section-links' });
|
|
200
|
+
groupOf.set(section.id, group);
|
|
201
|
+
}
|
|
202
|
+
groups.push(group);
|
|
203
|
+
return group;
|
|
204
|
+
};
|
|
205
|
+
for (const area of areas) {
|
|
206
|
+
const section = area.section ? sections.get(area.section) : null;
|
|
207
|
+
const last = groups.at(-1);
|
|
208
|
+
const group = section
|
|
209
|
+
? groupOf.get(section.id) ?? makeGroup(section)
|
|
210
|
+
: last && !last.section ? last : makeGroup(null);
|
|
211
|
+
group.areas.push(area);
|
|
212
|
+
}
|
|
213
|
+
for (const section of sections.values()) if (!groupOf.has(section.id)) makeGroup(section);
|
|
214
|
+
const ordered = [...groups.filter((group) => !group.section?.footer), ...groups.filter((group) => group.section?.footer)];
|
|
215
|
+
groups.splice(0, groups.length, ...ordered);
|
|
216
|
+
for (const group of groups) {
|
|
217
|
+
group.element.append(...[group.headingText, group.headingLink, group.linkSlot, ...group.areas.map((area) => areaLinks.get(area.id))].filter(Boolean));
|
|
218
|
+
nav.append(group.element);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderHeading(group) {
|
|
223
|
+
const { section, headingText, headingLink } = group;
|
|
224
|
+
const screen = section.screenId ? screens.get(section.screenId) : null;
|
|
225
|
+
const opens = Boolean(screen && isShown(screen.id) && !screen.external);
|
|
226
|
+
headingText.hidden = !section.label || opens;
|
|
227
|
+
headingLink.hidden = !section.label || !opens;
|
|
228
|
+
if (opens) headingLink.setAttribute('href', screenHref(screen));
|
|
229
|
+
else headingLink.removeAttribute('href');
|
|
230
|
+
if (opens && activeScreenId === screen.id) headingLink.setAttribute('aria-current', 'page');
|
|
231
|
+
else headingLink.removeAttribute('aria-current');
|
|
232
|
+
return opens;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderLinks(group) {
|
|
236
|
+
group.linkSlot.replaceChildren(...group.section.links.map((entry) => {
|
|
237
|
+
const link = makeElement(doc, 'a', {
|
|
238
|
+
className: linkClass,
|
|
239
|
+
attrs: { href: entry.href, 'data-link': entry.id, 'aria-current': entry.current ? 'page' : null },
|
|
240
|
+
});
|
|
241
|
+
link.append(makeElement(doc, 'span', { className: 'bb-area-link-label', text: entry.label }));
|
|
242
|
+
if (entry.count > 0) link.append(countBadge(entry.count, `待っているもの${entry.count}件`));
|
|
243
|
+
link.addEventListener('click', (event) => onSelectLink?.(entry.id, event));
|
|
244
|
+
return link;
|
|
245
|
+
}));
|
|
246
|
+
group.linkSlot.hidden = group.section.links.length === 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderGroup(group) {
|
|
250
|
+
const headingOpens = group.section ? renderHeading(group) : false;
|
|
251
|
+
if (group.section) renderLinks(group);
|
|
252
|
+
const anyArea = group.areas.some((area) => !areaLinks.get(area.id).hidden);
|
|
253
|
+
group.element.hidden = !(anyArea || headingOpens || (group.section?.links.length ?? 0) > 0);
|
|
152
254
|
}
|
|
153
255
|
|
|
154
256
|
function renderAreaLink(area) {
|
|
@@ -202,6 +304,7 @@ export function createAreaNav({
|
|
|
202
304
|
|
|
203
305
|
function render() {
|
|
204
306
|
for (const area of areas) renderAreaLink(area);
|
|
307
|
+
for (const group of groups) renderGroup(group);
|
|
205
308
|
renderTabs();
|
|
206
309
|
}
|
|
207
310
|
|
|
@@ -241,6 +344,23 @@ export function createAreaNav({
|
|
|
241
344
|
render();
|
|
242
345
|
return controller;
|
|
243
346
|
},
|
|
347
|
+
/**
|
|
348
|
+
* Links at the top of a section, before its areas (for example starred
|
|
349
|
+
* projects): `{id, label, href, count?, current?}`. An empty list removes them.
|
|
350
|
+
*/
|
|
351
|
+
setLinks(sectionId, links) {
|
|
352
|
+
const section = sections.get(sectionId);
|
|
353
|
+
if (!section) return controller;
|
|
354
|
+
section.links = (links ?? []).filter((entry) => entry && entry.id && entry.label && entry.href).map((entry) => ({
|
|
355
|
+
id: String(entry.id),
|
|
356
|
+
label: String(entry.label),
|
|
357
|
+
href: String(entry.href),
|
|
358
|
+
count: Number.isInteger(entry.count) && entry.count > 0 ? entry.count : 0,
|
|
359
|
+
current: entry.current === true,
|
|
360
|
+
}));
|
|
361
|
+
render();
|
|
362
|
+
return controller;
|
|
363
|
+
},
|
|
244
364
|
};
|
|
245
365
|
render();
|
|
246
366
|
return controller;
|
package/ui/inbox.css
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 受信箱. Load after workspace-kit.css.
|
|
3
|
+
* The two tabs, the sections by what to do, the rows with their buttons on
|
|
4
|
+
* the right, and the lines for kinds that could not be read or are not
|
|
5
|
+
* collected yet. A kind that could not be read is told by words in the
|
|
6
|
+
* warning tone, never left out. Uses only the shared tokens in
|
|
7
|
+
* brainbase-tokens.css.
|
|
8
|
+
*/
|
|
9
|
+
.bb-inbox-tabs {
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: var(--bb-space-1);
|
|
12
|
+
margin: 0 0 var(--bb-space-4);
|
|
13
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.bb-inbox-tab {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 6px;
|
|
20
|
+
margin-bottom: -1px;
|
|
21
|
+
border: 0;
|
|
22
|
+
border-bottom: 2px solid transparent;
|
|
23
|
+
padding: 8px 12px;
|
|
24
|
+
background: transparent;
|
|
25
|
+
color: var(--bb-color-muted);
|
|
26
|
+
font: inherit;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.bb-inbox-tab.is-selected {
|
|
32
|
+
border-bottom-color: var(--bb-color-accent);
|
|
33
|
+
color: var(--bb-color-ink);
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.bb-inbox-count {
|
|
38
|
+
min-width: 20px;
|
|
39
|
+
border-radius: 999px;
|
|
40
|
+
padding: 0 6px;
|
|
41
|
+
background: var(--bb-color-accent-soft);
|
|
42
|
+
color: var(--bb-color-accent);
|
|
43
|
+
font-size: 11px;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
line-height: 18px;
|
|
46
|
+
text-align: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.bb-inbox-loading {
|
|
50
|
+
height: 2px;
|
|
51
|
+
margin: 0 0 10px;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
background: var(--bb-color-accent-soft);
|
|
54
|
+
color: transparent;
|
|
55
|
+
font-size: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.bb-inbox-group {
|
|
59
|
+
margin: 0 0 var(--bb-space-5);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.bb-inbox-group-title {
|
|
63
|
+
margin: 0 0 var(--bb-space-2);
|
|
64
|
+
color: var(--bb-color-muted);
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bb-inbox-list {
|
|
70
|
+
border: 1px solid var(--bb-color-border);
|
|
71
|
+
border-radius: var(--bb-radius-md);
|
|
72
|
+
background: var(--bb-color-paper);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.bb-inbox-row {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--bb-space-2) var(--bb-space-3);
|
|
80
|
+
padding: var(--bb-space-3) var(--bb-space-4);
|
|
81
|
+
border-top: 1px solid var(--bb-color-border);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.bb-inbox-row:first-child {
|
|
85
|
+
border-top: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.bb-inbox-row-main {
|
|
89
|
+
flex: 1 1 260px;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.bb-inbox-title {
|
|
94
|
+
color: var(--bb-color-ink);
|
|
95
|
+
font-size: 14px;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
overflow-wrap: anywhere;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
a.bb-inbox-title:hover,
|
|
102
|
+
a.bb-inbox-title:focus-visible {
|
|
103
|
+
color: var(--bb-color-accent-strong);
|
|
104
|
+
text-decoration: underline;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.bb-inbox-detail {
|
|
108
|
+
margin: 2px 0 0;
|
|
109
|
+
color: var(--bb-color-muted);
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.bb-inbox-meta {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-wrap: wrap;
|
|
116
|
+
gap: var(--bb-space-2);
|
|
117
|
+
margin-top: 4px;
|
|
118
|
+
color: var(--bb-color-muted);
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.bb-inbox-blocking {
|
|
123
|
+
border-radius: 999px;
|
|
124
|
+
padding: 0 8px;
|
|
125
|
+
background: var(--bb-color-warning-soft);
|
|
126
|
+
color: var(--bb-color-warning);
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bb-inbox-due {
|
|
131
|
+
color: var(--bb-color-ink);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.bb-inbox-row-actions {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-wrap: wrap;
|
|
137
|
+
gap: var(--bb-space-1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.bb-inbox-later-menu {
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-wrap: wrap;
|
|
143
|
+
flex-basis: 100%;
|
|
144
|
+
justify-content: flex-end;
|
|
145
|
+
gap: var(--bb-space-1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.bb-inbox-source {
|
|
149
|
+
margin: var(--bb-space-2) 0 0;
|
|
150
|
+
font-size: 13px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.bb-inbox-source.is-unavailable {
|
|
154
|
+
border-radius: var(--bb-radius-sm);
|
|
155
|
+
padding: var(--bb-space-2) var(--bb-space-3);
|
|
156
|
+
background: var(--bb-color-warning-soft);
|
|
157
|
+
color: var(--bb-color-warning);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.bb-inbox-source.is-not-connected,
|
|
161
|
+
.bb-inbox-uncollected {
|
|
162
|
+
color: var(--bb-color-muted);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.bb-inbox-uncollected {
|
|
166
|
+
margin: 0 0 var(--bb-space-3);
|
|
167
|
+
font-size: 13px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.bb-inbox-empty {
|
|
171
|
+
margin: 0 0 var(--bb-space-3);
|
|
172
|
+
color: var(--bb-color-ink);
|
|
173
|
+
font-size: 14px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.bb-inbox-elsewhere {
|
|
177
|
+
display: flex;
|
|
178
|
+
gap: var(--bb-space-2);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@media (max-width: 640px) {
|
|
182
|
+
.bb-inbox-row {
|
|
183
|
+
padding: var(--bb-space-3);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.bb-inbox-row-actions {
|
|
187
|
+
width: 100%;
|
|
188
|
+
}
|
|
189
|
+
}
|