@unson/brainbase-mcp 0.41.10 → 0.41.13

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.
@@ -1,4 +1,5 @@
1
1
  @import './native-scrollbars.css';
2
+ @import './area-nav.css';
2
3
  /*
3
4
  * Shell of the local Web host, in the organization edition's layout: a white
4
5
  * left rail (brand, navigation, source) and a paper workspace. Uses only the
@@ -71,19 +72,6 @@ body {
71
72
  gap: 2px;
72
73
  }
73
74
 
74
- .bb-shell-nav-group {
75
- display: grid;
76
- gap: 2px;
77
- }
78
-
79
- .bb-shell-nav-group-head {
80
- margin: 14px 10px 4px;
81
- color: var(--bb-color-rail-muted);
82
- font-size: 11px;
83
- font-weight: 700;
84
- letter-spacing: .04em;
85
- }
86
-
87
75
  .bb-shell-nav-link {
88
76
  min-height: 38px;
89
77
  display: flex;
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { createGraphProjectsView } from './graph-projects-view.js';
19
19
  import { createGraphRegistryView } from './graph-registry-view.js';
20
+ import { createGraphOntologyView } from './graph-ontology-view.js';
20
21
  import { createPeopleAndCompaniesView } from './people-and-companies-view.js';
21
22
  import { createObjectiveEditorController } from './objective-editor.js';
22
23
  import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
@@ -27,6 +28,7 @@ import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-
27
28
  import { createWorldModelView } from './world-model-view.js';
28
29
  import { createGraphClient } from './graph-view-shared.js';
29
30
  import { installNativeScrollbars } from './native-scrollbars.js';
31
+ import { browserAreaStorage, createAreaMemory, createAreaNav } from './area-nav.js';
30
32
 
31
33
  export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
32
34
 
@@ -51,22 +53,30 @@ const NAV_ICON_PATHS = Object.freeze({
51
53
  minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
52
54
  // Two people (the organization edition's 人と会社 uses the same shape).
53
55
  people: ['M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M2.5 20a6.5 6.5 0 0 1 13 0', 'M16 4.5a3.5 3.5 0 0 1 0 6.5', 'M18 14a6.5 6.5 0 0 1 3.5 6'],
56
+ // Kinds of information and how they connect (settings, ledger D14): three linked boxes.
57
+ ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
54
58
  // World view (extension id `world`, ledger P17).
55
59
  world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
60
+ // Areas whose screens have their own icons (ledger D10, stage 2): a briefcase, and
61
+ // the organization edition's gear.
62
+ 'area:work': ['M4 8h16v11H4z', 'M9 8V5h6v3', 'M4 13h16'],
63
+ 'area:settings': ['M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
56
64
  });
57
65
  // Screens without their own icon (for example a private product's extension).
58
66
  const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
59
67
  const SVG_NS = 'http://www.w3.org/2000/svg';
60
68
 
61
- // The menu's areas (brainbase-project ledger D10): the same order and headings
62
- // as the organization edition. 目的と現状 is the only item of its area, so it
63
- // has no heading. Inside an area, `order` lists the screens first in that order.
69
+ // The menu's areas (brainbase-project ledger D10): the same order and names as
70
+ // the organization edition. The left side lists only the areas; the screens of
71
+ // the current area are tabs above the page (stage 2). 目的と現状 is the only
72
+ // screen of its area and keeps its own name. Inside an area, `order` lists the
73
+ // screens first in that order; `defaultScreen` opens when nothing is remembered.
64
74
  const NAV_AREAS = Object.freeze([
65
- Object.freeze({ id: 'purpose', label: null, order: ['objectives'] }),
66
- Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'] }),
67
- Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'] }),
68
- Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'] }),
69
- Object.freeze({ id: 'settings', label: '設定', order: [] }),
75
+ Object.freeze({ id: 'purpose', label: null, order: ['objectives'], icon: 'objectives' }),
76
+ Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'], defaultScreen: 'projects', icon: 'area:work' }),
77
+ Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'], icon: 'value-proofs' }),
78
+ Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'], defaultScreen: 'graph', icon: 'graph' }),
79
+ Object.freeze({ id: 'settings', label: '設定', order: ['ontology'], defaultScreen: 'ontology', icon: 'area:settings' }),
70
80
  ]);
71
81
 
72
82
  // Menu order only: the screens' own order (and so the first screen) stays as given.
@@ -88,6 +98,23 @@ function navLayout(screens) {
88
98
  return { leading, areas, trailing };
89
99
  }
90
100
 
101
+ // Every menu line is an area: a screen without an area is an area of its own.
102
+ function navAreas(screens) {
103
+ const layout = navLayout(screens);
104
+ const own = (screen) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id });
105
+ return [
106
+ ...layout.leading.map(own),
107
+ ...layout.areas.map(({ area, screens: members }) => ({
108
+ id: area.id,
109
+ label: area.label,
110
+ defaultScreen: area.defaultScreen,
111
+ screenIds: members.map((screen) => screen.id),
112
+ icon: area.icon,
113
+ })),
114
+ ...layout.trailing.map(own),
115
+ ];
116
+ }
117
+
91
118
  function makeIcon(doc, id) {
92
119
  const paths = NAV_ICON_PATHS[id] ?? DEFAULT_NAV_ICON_PATHS;
93
120
  const create = (tag) => (typeof doc.createElementNS === 'function' ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag));
@@ -186,6 +213,21 @@ function makePage(container, context) {
186
213
  return page;
187
214
  }
188
215
 
216
+ /** Names of judgment references from the local Graph, under the host's current access. */
217
+ function judgmentReferenceLabelReader(context) {
218
+ const client = createGraphClient({ fetcher: context.fetcher, basePath: '/api/graph', token: context.token });
219
+ return async (references) => {
220
+ const labels = new Map();
221
+ await Promise.all(references.map(async ({ ref }) => {
222
+ const result = await client.read(`/entities/${encodeURIComponent(ref)}`);
223
+ const entity = result.state === 'ok' && isRecord(result.payload?.entity) ? result.payload.entity : null;
224
+ const name = entity && entity.id === ref && typeof entity.name === 'string' ? entity.name.trim() : '';
225
+ if (name) labels.set(ref, name);
226
+ }));
227
+ return labels;
228
+ };
229
+ }
230
+
189
231
  function mountToday(container, context) {
190
232
  return createJudgmentHistoryUI({
191
233
  root: makePage(container, context),
@@ -195,6 +237,7 @@ function mountToday(container, context) {
195
237
  fetcher: context.fetcher,
196
238
  token: context.token,
197
239
  basePath: '/api/judgment-history',
240
+ resolveReferenceLabels: judgmentReferenceLabelReader(context),
198
241
  });
199
242
  }
200
243
 
@@ -283,7 +326,7 @@ function mountGraphScreen(screenId, createView, extra = {}) {
283
326
  fetcher: context.fetcher,
284
327
  token: context.token,
285
328
  initialEntityId: context.initialEntityId,
286
- ...extra,
329
+ ...(typeof extra === 'function' ? extra(context) : extra),
287
330
  });
288
331
  };
289
332
  }
@@ -293,8 +336,13 @@ export const LOCAL_WEB_SCREENS = Object.freeze([
293
336
  Object.freeze({ id: 'minutes', label: '議事録', area: 'work', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
294
337
  Object.freeze({ id: 'objectives', label: '目的と現状', area: 'purpose', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
295
338
  Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', area: 'work', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
296
- Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView) }),
339
+ // A record of 情報と関係 opens as its own screen at `#graph?entity_id=…`; the list is `#graph`.
340
+ Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
341
+ onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
342
+ })) }),
297
343
  Object.freeze({ id: 'people', label: '人と会社', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
344
+ // The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
345
+ Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', area: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
298
346
  ]);
299
347
 
300
348
  function renderGraphGate(doc, slot, status, onRecheck, label) {
@@ -378,6 +426,8 @@ export function createLocalWebShell({
378
426
  initialScreen,
379
427
  meetingMinutesStorage = null,
380
428
  meetingMinutesLineage = null,
429
+ areaMemory = createAreaMemory(browserAreaStorage()),
430
+ onNavigate,
381
431
  } = {}) {
382
432
  if (!root) throw new TypeError('root is required');
383
433
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
@@ -385,12 +435,13 @@ export function createLocalWebShell({
385
435
  const request = typeof fetcher === 'function'
386
436
  ? fetcher
387
437
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
388
- const context = Object.freeze({ document: doc, fetcher: request, token, meetingMinutesStorage, meetingMinutesLineage });
438
+ // `navigate(target, { back })` lets a screen give its own state an address (the host's hash).
439
+ const navigate = typeof onNavigate === 'function' ? (target, options = {}) => onNavigate(target, options) : null;
440
+ const context = Object.freeze({ document: doc, fetcher: request, token, meetingMinutesStorage, meetingMinutesLineage, navigate });
389
441
 
390
442
  const status = { phase: 'loading', data: null, error: null };
391
443
  const mounted = new Map();
392
444
  const slots = new Map();
393
- const links = new Map();
394
445
  const pendingEntityIds = new Map();
395
446
  let active = null;
396
447
 
@@ -411,37 +462,27 @@ export function createLocalWebShell({
411
462
  };
412
463
  menuButton.addEventListener('click', () => setMenuOpen(!sidebar.className.includes('is-open')));
413
464
  backdrop.addEventListener('click', () => setMenuOpen(false));
414
- const navLink = (screen) => {
415
- const link = makeElement(doc, 'a', { className: 'bb-shell-nav-link', text: screen.label, attrs: { href: `#${screen.id}` } });
416
- // The label stays the link's own text; the icon goes in front where the DOM allows it.
417
- if (typeof link.prepend === 'function') link.prepend(makeIcon(doc, screen.id));
418
- link.addEventListener('click', () => {
465
+ const areaList = navAreas(screens);
466
+ const areaNav = createAreaNav({
467
+ document: doc,
468
+ areas: areaList,
469
+ screens: screens.map((screen) => ({ id: screen.id, label: screen.label })),
470
+ memory: areaMemory,
471
+ linkClassName: 'bb-shell-nav-link',
472
+ iconFor: (area) => makeIcon(doc, areaList.find((entry) => entry.id === area.id)?.icon ?? area.id),
473
+ onSelect: (screenId) => {
419
474
  setMenuOpen(false);
420
- controller.show(screen.id);
421
- });
422
- links.set(screen.id, link);
423
- return link;
424
- };
425
- const layout = navLayout(screens);
426
- for (const screen of layout.leading) nav.append(navLink(screen));
427
- for (const { area, screens: members } of layout.areas) {
428
- if (!area.label) {
429
- for (const screen of members) nav.append(navLink(screen));
430
- continue;
431
- }
432
- const headId = `bb-shell-nav-area-${area.id}`;
433
- const group = makeElement(doc, 'div', { className: 'bb-shell-nav-group', attrs: { role: 'group', 'aria-labelledby': headId } });
434
- group.append(makeElement(doc, 'div', { className: 'bb-shell-nav-group-head', text: area.label, attrs: { id: headId } }));
435
- for (const screen of members) group.append(navLink(screen));
436
- nav.append(group);
437
- }
438
- for (const screen of layout.trailing) nav.append(navLink(screen));
475
+ controller.show(screenId);
476
+ },
477
+ });
478
+ nav.append(areaNav.nav);
439
479
  const source = makeElement(doc, 'section', { className: 'bb-shell-source', attrs: { 'aria-label': '出典' } });
440
480
  sidebar.append(brand, nav, source);
441
481
  const workspace = makeElement(doc, 'div', { className: 'bb-shell-workspace' });
442
482
  const mobileBar = makeElement(doc, 'div', { className: 'bb-shell-mobile-bar' });
443
483
  mobileBar.append(makeElement(doc, 'strong', { text: 'Brainbase' }), menuButton);
444
484
  const main = makeElement(doc, 'main', { className: 'bb-shell-main' });
485
+ main.append(areaNav.tabs);
445
486
  for (const screen of screens) {
446
487
  const slot = makeElement(doc, 'section', { className: 'bb-shell-screen', attrs: { 'data-screen': screen.id, 'aria-label': screen.label } });
447
488
  slot.hidden = true;
@@ -477,6 +518,8 @@ export function createLocalWebShell({
477
518
  if (mounted.has(screen.id)) {
478
519
  const view = mounted.get(screen.id);
479
520
  if (entityId && screen.id === 'graph' && typeof view?.openEntity === 'function') void view.openEntity(entityId);
521
+ // `#graph` without a record is the list: a record still open there closes (the browser's back).
522
+ if (!entityId && screen.id === 'graph' && view?.state?.detail && typeof view.closeEntity === 'function') view.closeEntity();
480
523
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
481
524
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
482
525
  return;
@@ -510,10 +553,7 @@ export function createLocalWebShell({
510
553
  : screen.id === 'projects' ? parsed.projectId ?? null : null);
511
554
  active = screen.id;
512
555
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
513
- for (const [screenId, link] of links) {
514
- if (screenId === screen.id) link.setAttribute('aria-current', 'page');
515
- else link.removeAttribute('aria-current');
516
- }
556
+ areaNav.show(screen.id);
517
557
  ensureMounted(screen, pendingEntityIds.get(screen.id));
518
558
  updateRail();
519
559
  return controller;
@@ -4,7 +4,7 @@
4
4
  --bb-color-accent-strong: var(--bb-color-brand-indigo);
5
5
  --bb-color-action: var(--bb-color-brand-indigo);
6
6
  display: grid;
7
- grid-template-rows: auto auto minmax(0, 1fr);
7
+ grid-template-rows: auto auto auto minmax(0, 1fr);
8
8
  gap: 4px;
9
9
  height: 100%;
10
10
  min-height: 0;
@@ -108,6 +108,70 @@
108
108
  color: var(--bb-color-danger);
109
109
  }
110
110
 
111
+ .bb-minutes-toolbar {
112
+ display: flex;
113
+ flex-wrap: wrap;
114
+ gap: 8px 12px;
115
+ align-items: flex-end;
116
+ min-width: 0;
117
+ padding: 8px 12px;
118
+ border: 1px solid var(--bb-color-border);
119
+ border-radius: var(--bb-radius-md, 6px);
120
+ background: var(--bb-color-surface);
121
+ }
122
+
123
+ .bb-minutes-filter-host {
124
+ display: contents;
125
+ }
126
+
127
+ .bb-minutes-filter-field {
128
+ display: grid;
129
+ gap: 2px;
130
+ min-width: 0;
131
+ }
132
+
133
+ .bb-minutes-filter-field[hidden],
134
+ .bb-minutes-filter-host[hidden],
135
+ .bb-minutes-filter-reset[hidden] {
136
+ display: none;
137
+ }
138
+
139
+ .bb-minutes-filter-field.is-keyword {
140
+ flex: 1 1 180px;
141
+ max-width: 260px;
142
+ }
143
+
144
+ .bb-minutes-filter-field .bb-minutes-input {
145
+ min-height: 38px;
146
+ padding: 4px 8px;
147
+ font-size: 13px;
148
+ }
149
+
150
+ .bb-minutes-filter-period {
151
+ display: flex;
152
+ gap: 4px;
153
+ align-items: center;
154
+ }
155
+
156
+ .bb-minutes-filter-period .bb-minutes-input {
157
+ width: 8.75rem;
158
+ }
159
+
160
+ .bb-minutes-filter-period-sep {
161
+ color: var(--bb-color-muted);
162
+ }
163
+
164
+ .bb-minutes-filter-combobox {
165
+ min-width: 11rem;
166
+ max-width: 16rem;
167
+ }
168
+
169
+ .bb-minutes-create-reason {
170
+ color: var(--bb-color-muted);
171
+ font-size: 12px;
172
+ overflow-wrap: anywhere;
173
+ }
174
+
111
175
  .bb-minutes-columns {
112
176
  display: grid;
113
177
  grid-template-columns: minmax(340px, 380px) minmax(0, 1fr);
@@ -537,6 +601,28 @@
537
601
  .bb-minutes-columns {
538
602
  grid-template-columns: 1fr;
539
603
  }
604
+
605
+ .bb-minutes-filter-field {
606
+ flex: 1 1 calc(50% - 6px);
607
+ max-width: none;
608
+ }
609
+
610
+ .bb-minutes-filter-field.is-keyword,
611
+ .bb-minutes-filter-field.is-period {
612
+ flex-basis: 100%;
613
+ max-width: none;
614
+ }
615
+
616
+ .bb-minutes-filter-period .bb-minutes-input {
617
+ flex: 1 1 0;
618
+ width: auto;
619
+ min-width: 0;
620
+ }
621
+
622
+ .bb-minutes-filter-combobox {
623
+ min-width: 0;
624
+ max-width: none;
625
+ }
540
626
  }
541
627
 
542
628
  /* この会議から: project, tasks, decisions, storage and sharing. */