@unson/brainbase-mcp 0.41.2 → 0.41.3

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/README.md CHANGED
@@ -265,3 +265,12 @@ After the workflow succeeds, verify npm `gitHead`, `dist.integrity`, and dist-ta
265
265
  - [Judgment DAG milestones](docs/management/judgment-dag-milestones.md)
266
266
 
267
267
  MIT License
268
+
269
+
270
+ ### Shared purpose overview presentation
271
+
272
+ The read-only foundation overview uses a four-column ledger (purpose, recorded state, outstanding checks and check date). A row opens the shared native detail dialog. Philosophy starts expanded; world models start folded. Hosts can receive failure and partial-coverage notification descriptors through `onNotice` without replacing authenticated readers or catalog validation. Owner-private objectives support an optional compact view and preserve bundle provenance.
273
+
274
+ Load `ui/native-scrollbars.css` and initialize `installNativeScrollbars(document)` from `ui/native-scrollbars.js` for native scrollbars that appear during scrolling and fade after inactivity. Dispose the returned callback when the host is destroyed. The local Web host serves and initializes this shared module.
275
+
276
+ See [the presentation contract](docs/specs/story-foundation-overview-readability.md) for acceptance conditions. Display fixtures belong in tests and local previews; they are never inserted into canonical records.
@@ -25,8 +25,8 @@
25
25
  },
26
26
  {
27
27
  "path": "README.md",
28
- "sha256": "c36fd404cbc6e9c2262e82b5d921ccbcef6617774f969dd1e0252cd9e7101af8"
28
+ "sha256": "55fc2b9c7945b81cfe3b7971bc1d067f8cd45d057fbf23dcf3dfae175838d1d3"
29
29
  }
30
30
  ],
31
- "digest": "1beccdf5679b9b7f42375a498a76a661d87ba665f66657c93827dd8d6a938c28"
31
+ "digest": "89b20a5aa122e73e9d2f7c99e1b029cb12b101b95574ee4560ce023ec3197304"
32
32
  }
@@ -52,7 +52,7 @@ export const LOCAL_WEB_JUDGMENT_HISTORY_PREFIX = JUDGMENT_HISTORY_HTTP_PREFIX;
52
52
  export const LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES = 64 * 1024;
53
53
  export const LOCAL_WEB_PROVENANCE_SOURCE_ID = 'brainbase-local-web';
54
54
  const CANONICAL_FILES = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
55
- const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css'];
55
+ const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css'];
56
56
  const CONTENT_TYPES = Object.freeze({
57
57
  '.js': 'text/javascript; charset=utf-8',
58
58
  '.css': 'text/css; charset=utf-8'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.2",
3
+ "version": "0.41.3",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -409,7 +409,9 @@
409
409
  "./private-philosophy-graph": {
410
410
  "types": "./dist/private-philosophy-graph.d.ts",
411
411
  "import": "./dist/private-philosophy-graph.js"
412
- }
412
+ },
413
+ "./ui/native-scrollbars": "./ui/native-scrollbars.js",
414
+ "./ui/native-scrollbars.css": "./ui/native-scrollbars.css"
413
415
  },
414
416
  "bin": {
415
417
  "brainbase-mcp": "dist/index.js",
@@ -481,5 +483,5 @@
481
483
  "engines": {
482
484
  "node": ">=20"
483
485
  },
484
- "gitHead": "193d1d1fea9d177b04a3c6b01069aef202979526"
486
+ "gitHead": "19cc7821541c7e563e4d08fd4b2b09936e80a701"
485
487
  }
@@ -50,6 +50,23 @@
50
50
  --bb-color-rail-active: #efedff;
51
51
  --bb-color-rail-strong: #202124;
52
52
  --bb-color-overlay: rgb(32 33 36 / 40%);
53
+ --bb-color-notice-border: #ffffff20;
54
+ --bb-color-notice-danger: #e29494;
55
+ --bb-color-notice-warning: #d9a441;
56
+ --bb-color-notice-background: linear-gradient(110deg,rgb(13 25 46 / 88%),rgb(36 50 75 / 72%));
57
+ --bb-color-notice-text: #ffffff;
58
+ --bb-color-notice-danger-symbol-border: #e2949470;
59
+ --bb-color-notice-danger-text: #f2bbbb;
60
+ --bb-color-notice-warning-text: #f4d68a;
61
+ --bb-color-notice-warning-symbol-border: #d9a44180;
62
+ --bb-color-notice-control-border: #ffffff45;
63
+ --bb-color-inline-error: #9b3d3d;
64
+ --bb-color-record-missing: #80601c;
65
+ --bb-color-dialog-overlay: #0d192e66;
66
+ --bb-color-scrollbar-rgb: 36,50,75;
67
+ --bb-color-scrollbar-dark-rgb: 203,212,225;
68
+ --bb-shadow-help: 0 6px 24px #0d192e20;
69
+ --bb-font-help: Georgia,serif;
53
70
  --bb-shadow: 0 16px 42px rgb(32 33 36 / 10%);
54
71
  --bb-font-sans: Geist, "Noto Sans JP", "Hiragino Sans", system-ui, sans-serif;
55
72
  --bb-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -237,3 +237,21 @@
237
237
 
238
238
  /* Catalog provenance is secondary to the topic, but remains visible on every card. */
239
239
  .bb-fov-source { display: inline-block; margin: 0 0 12px; padding: 4px 10px; border-radius: 999px; background: var(--bb-surface-subtle, #f4f4f8); color: var(--bb-text-muted, #62626e); font-size: 12px; overflow-wrap: anywhere; }
240
+
241
+ .bb-fov-objective-ledger .bb-ws-ledger-row { grid-template-columns: minmax(180px,1.2fr) minmax(200px,1.3fr) minmax(150px,1fr) 110px; }
242
+ .bb-fov-objective-ledger .bb-ws-ledger-row > span { white-space: normal; line-height: 1.7; }
243
+ .bb-fov-objective-ledger .bb-ws-ledger-row:is(.is-selected,:hover) { background: var(--bb-color-accent-soft); box-shadow: inset 3px 0 var(--bb-color-accent); }
244
+ .bb-fov .bb-ws-section-title { display: flex; align-items: center; gap: 8px; }
245
+ .bb-fov-philosophy-grid { grid-template-columns: minmax(0,1fr); }
246
+ .bb-fov-philosophy-card { border-top: 0; width: 100%; box-sizing: border-box; }
247
+ .bb-fov-reading { display: grid; gap: 26px; }
248
+ .bb-fov-reading > section > h3 { margin: 0 0 10px; color: var(--bb-color-muted); font-size: 12px; font-weight: 600; line-height: 1.5; }
249
+ .bb-fov-reading .bb-ws-definition { margin: 0; grid-template-columns: 100px minmax(0,1fr); gap: 10px 20px; font-size: 14px; line-height: 1.8; }
250
+ .bb-fov-reading .bb-fov-current-state { padding: 16px; border: 1px solid var(--bb-color-border); border-radius: 10px; background: var(--bb-color-canvas); gap: 12px; }
251
+ .bb-fov-original { margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--bb-color-border); font-size: 12px; color: var(--bb-color-muted); }
252
+ .bb-fov-original > summary { cursor: pointer; }
253
+ .bb-fov-missing { color: var(--bb-color-record-missing); }
254
+ @media (max-width: 760px) { .bb-fov-objective-ledger { overflow-x: auto; } .bb-fov-objective-ledger .bb-ws-ledger-row { min-width: 760px; } }
255
+ @media (max-width: 480px) { .bb-fov-reading .bb-ws-definition { grid-template-columns: 1fr; gap: 4px; } .bb-fov-reading dd { margin-bottom: 12px; } }
256
+ .bb-fov-philosophy-card .bb-fov-original { border-top: 0; padding-top: 0; }
257
+ .bb-fov-objective-ledger .bb-ws-ledger-row:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 2px; }
@@ -14,6 +14,7 @@ import {
14
14
  workspaceNotice,
15
15
  workspacePageHeader,
16
16
  workspaceSectionTitle,
17
+ workspaceHelp, workspaceDialog, workspaceDisclosure, workspaceErrorCard,
17
18
  } from './workspace-kit.js';
18
19
 
19
20
  export const FOUNDATION_OVERVIEW_CONTRACT_VERSION = 'foundation-overview.v1';
@@ -429,59 +430,66 @@ function objectiveCard(doc, record, variableMap) {
429
430
  return card;
430
431
  }
431
432
 
433
+ function objectiveReading(doc, record, variableMap, recordContexts) {
434
+ const definition = recordDefinition(record), details = detailsFor(record);
435
+ const body = makeElement(doc, 'div', { className: 'bb-fov-reading' });
436
+ const section = (title, content) => {
437
+ const part = makeElement(doc, 'section');
438
+ part.append(makeElement(doc, 'h3', { text: title }), content); body.append(part);
439
+ };
440
+ section('達成条件', workspaceDefinition(doc, [
441
+ ['目指す状態', formatAny(definition.desiredState)],
442
+ ['評価基準', criteriaElement(doc, definition, details, variableMap)],
443
+ ['評価期間', formatAny(firstValue(details, 'evaluation_period_note', 'evaluationPeriodNote')) ?? formatPeriod(definition.evaluationPeriod)],
444
+ ]));
445
+ section('確認の根拠', recordedStateElement(doc, record));
446
+ const provenance = Array.isArray(definition.provenance) ? definition.provenance : [];
447
+ section('出典', workspaceDefinition(doc, [
448
+ ['記録元', recordContexts?.get(record)?.label ?? formatAny(record.source) ?? '未記録'],
449
+ ['根拠', provenance.length ? formatStructuredValue(provenance) : '未記録'],
450
+ ]));
451
+ const original = makeElement(doc, 'details', { className: 'bb-fov-original' });
452
+ original.append(makeElement(doc, 'summary', { text: '記録の詳細・参照・原文' }), objectiveCard(doc, record, variableMap),
453
+ workspaceDefinition(doc, [['目的ID', definition.id], ['版', definition.revision], ['digest', record.digest]]));
454
+ body.append(original); return body;
455
+ }
456
+
457
+ function topicTitle(doc, title, text) {
458
+ const head = workspaceSectionTitle(doc, { title });
459
+ head.append(workspaceHelp(doc, { label: title, text })); return head;
460
+ }
461
+
432
462
  function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
433
463
  const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '目的' } });
434
- section.append(workspaceSectionTitle(doc, {
435
- title: '目的',
436
- lead: '目指す状態、評価の定義、記録上の現状を分けて確認します。',
437
- }));
464
+ section.append(topicTitle(doc, '目的', '目指す状態、評価の定義、記録上の現状を分けて確認します。'));
438
465
  if (catalog.objectives.length === 0) {
439
466
  section.append(workspaceNotice(doc, { label: '目的', text: 'この対象範囲には登録された目的がありません。' }));
440
467
  return section;
441
468
  }
442
- const variableMap = new Map(catalog.variables.map((record) => [foundationKey(recordDefinition(record)), record]));
443
- const rows = catalog.objectives.map((record) => {
444
- const definition = recordDefinition(record);
445
- const details = detailsFor(record);
469
+ const variableMap = new Map(catalog.variables.map(record => [foundationKey(recordDefinition(record)), record]));
470
+ const rows = catalog.objectives.map(record => {
471
+ const definition = recordDefinition(record), details = detailsFor(record), current = currentStateFor(record);
446
472
  const key = recordContexts?.get(record)?.key ?? `${definition.id}@${definition.revision}`;
447
473
  return {
448
- key,
449
- selected: key === viewState.objectiveKey,
474
+ key, selected: key === viewState.objectiveKey,
450
475
  onSelect: () => {
451
476
  viewState.objectiveKey = key;
452
- callbacks.render?.();
477
+ const opener = doc.activeElement;
478
+ viewState.dialog?.close();
479
+ viewState.dialog = workspaceDialog(doc, {
480
+ title: formatAny(details.title) ?? formatAny(definition.meaning) ?? definition.id, opener,
481
+ content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts),
482
+ });
453
483
  },
454
484
  cells: [
455
- { primary: formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? definition.id, secondary: recordContexts?.get(record)?.label ?? `${definition.id}@${definition.revision}` },
456
- formatAny(definition.desiredState) ?? '目指す状態: 未記録',
457
- formatAny(definition.adoptionState) ? (ADOPTION_LABELS[definition.adoptionState] ?? definition.adoptionState) : '状態未確認',
485
+ { primary: formatAny(details.title) ?? formatAny(definition.meaning) ?? definition.id },
486
+ current.text ?? current.status ?? '未記録',
487
+ { text: current.missing.length ? current.missing.join('・') : missingObjectiveFields(record).join('・') || '記録なし', className: 'bb-fov-missing' },
488
+ formatDate(current.asOf),
458
489
  ],
459
490
  };
460
491
  });
461
- section.append(workspaceLedger(doc, {
462
- className: 'bb-fov-objective-ledger',
463
- ariaLabel: '登録された目的',
464
- columns: ['目的', '目指す状態', '登録状態'],
465
- rows,
466
- }));
467
- const selected = catalog.objectives.find((record) => {
468
- const definition = recordDefinition(record);
469
- return (recordContexts?.get(record)?.key ?? `${definition.id}@${definition.revision}`) === viewState.objectiveKey;
470
- }) ?? (recordContexts ? null : catalog.objectives[0]);
471
- if (!selected && recordContexts) section.append(makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '目的の行を選ぶと、評価基準と記録上の現状を確認できます。' }));
472
- if (selected) {
473
- const selectedDefinition = recordDefinition(selected);
474
- viewState.objectiveKey = recordContexts?.get(selected)?.key ?? `${selectedDefinition.id}@${selectedDefinition.revision}`;
475
- if (catalog.objectives.length > 1) {
476
- section.append(workspaceNotice(doc, {
477
- label: '詳細表示',
478
- text: `目的${catalog.objectives.length}件のうち1件を選択して詳細を表示しています。`,
479
- }));
480
- }
481
- const card = objectiveCard(doc, selected, recordContexts?.get(selected)?.variableMap ?? variableMap);
482
- appendRecordSource(doc, card, selected, recordContexts);
483
- section.append(card);
484
- }
492
+ section.append(workspaceLedger(doc, { className: 'bb-fov-objective-ledger', ariaLabel: '登録された目的', columns: ['目的', '現状', '確認待ち', '確認日'], rows }));
485
493
  return section;
486
494
  }
487
495
 
@@ -506,9 +514,7 @@ function philosophyCard(doc, record) {
506
514
  const card = makeElement(doc, 'article', { className: 'bb-fov-philosophy-card' });
507
515
  const title = formatAny(payload.title) ?? record.id;
508
516
  card.append(makeElement(doc, 'h3', { text: title }));
509
- const rawStatus = formatAny(payload.status);
510
- const status = rawStatus ? (PHILOSOPHY_STATUS_LABELS[rawStatus] ?? `未確認(${rawStatus})`) : '状態未確認';
511
- card.append(makeElement(doc, 'p', { className: 'bb-fov-status', text: `状態: ${status} / 版: ${record.revision}` }));
517
+
512
518
  if (formatAny(payload.statement)) card.append(makeElement(doc, 'p', { className: 'bb-fov-philosophy-statement', text: formatAny(payload.statement) }));
513
519
  if (formatAny(payload.summary)) card.append(makeElement(doc, 'p', { text: formatAny(payload.summary) }));
514
520
  const applicability = record.applicability?.scope
@@ -523,10 +529,7 @@ function philosophyCard(doc, record) {
523
529
 
524
530
  function renderPhilosophies(doc, catalog, recordContexts) {
525
531
  const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '哲学' } });
526
- section.append(workspaceSectionTitle(doc, {
527
- title: '哲学',
528
- lead: '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。',
529
- }));
532
+
530
533
  if (catalog.philosophies.length === 0) {
531
534
  section.append(workspaceNotice(doc, { label: '哲学', text: 'この対象範囲には登録された哲学がありません。' }));
532
535
  return section;
@@ -534,8 +537,9 @@ function renderPhilosophies(doc, catalog, recordContexts) {
534
537
  const grid = makeElement(doc, 'div', { className: 'bb-fov-philosophy-grid' });
535
538
  for (const record of catalog.philosophies) {
536
539
  const card = philosophyCard(doc, record);
537
- appendRecordSource(doc, card, record, recordContexts);
538
- grid.append(card);
540
+ const refs = makeElement(doc, 'details', { className: 'bb-fov-original' });
541
+ refs.append(makeElement(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['出典', recordContexts?.get(record)?.label ?? '未記録'], ['状態', record.payload.status], ['版', record.revision], ['記録ID', record.id], ['digest', record.digest]]));
542
+ card.append(refs); grid.append(card);
539
543
  }
540
544
  section.append(grid);
541
545
  return section;
@@ -592,10 +596,7 @@ function modelCard(doc, record, variableMap) {
592
596
 
593
597
  function renderModels(doc, catalog, viewState, callbacks, recordContexts) {
594
598
  const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '世界モデル' } });
595
- section.append(workspaceSectionTitle(doc, {
596
- title: '世界モデル',
597
- lead: '変数の関係として登録された見方です。仮説・未検証・許可用途を、それぞれの状態として表示します。',
598
- }));
599
+
599
600
  const searchWrap = makeElement(doc, 'div', { className: 'bb-fov-model-search' });
600
601
  const label = makeElement(doc, 'label', { text: 'モデルを検索', attrs: { for: 'bb-fov-model-search' } });
601
602
  const input = makeElement(doc, 'input', {
@@ -682,6 +683,7 @@ export function renderFoundationOverview(root, state, {
682
683
  showHeader = true,
683
684
  recordContexts,
684
685
  partial = false,
686
+ onNotice,
685
687
  } = {}) {
686
688
  if (!root) throw new TypeError('root is required');
687
689
  const doc = getDocument(explicitDocument);
@@ -698,28 +700,26 @@ export function renderFoundationOverview(root, state, {
698
700
  source: `正本: ${scopeId}`,
699
701
  }));
700
702
  }
701
- if (!recordContexts) surface.append(workspaceNotice(doc, {
702
- label: '対象範囲',
703
- text: scopeId,
704
- }));
703
+ const notify = notice => {
704
+ if (onNotice) onNotice(notice);
705
+ else surface.append(workspaceErrorCard(doc, notice));
706
+ };
705
707
  if (state.status === 'loading') {
706
- surface.append(renderLoading(doc, state, callbacks));
707
- root.append(surface);
708
- return surface;
708
+ surface.append(renderLoading(doc, state, callbacks)); root.append(surface); return surface;
709
709
  }
710
710
  if (state.status !== 'ready') {
711
- surface.append(renderFailure(doc, state, callbacks));
712
- root.append(surface);
713
- return surface;
711
+ notify({ title: '取得失敗', message: `${state.error?.message ?? '目的・哲学・世界モデルを確認できません。'} 登録なし・0件として表示していません。`, onRetry: callbacks.load });
712
+ root.append(surface); return surface;
714
713
  }
715
714
  const catalog = state.catalog;
716
- if (partial) surface.append(workspaceNotice(doc, { label: '取得済みの範囲', text: '以下の件数・登録なし・検索結果は、取得できた記録だけを対象にしています。全体は未確認です。' }));
717
- surface.append(recordContexts
718
- ? makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '記録上の現状は保存時点の記述です。登録状態と検証状態を分けて読みます。' })
719
- : workspaceNotice(doc, { label: '読み方', text: '登録は達成・採用・検証済みを意味しません。記録上の現状は保存時点の記述です。' }));
715
+ if (partial) notify({ title: '取得済みの範囲', tone: 'warning', message: '以下の登録なし・検索結果は取得できた記録だけが対象です。全体は未確認です。' });
720
716
  surface.append(renderObjectives(doc, catalog, viewState, callbacks, recordContexts));
721
- surface.append(renderPhilosophies(doc, catalog, recordContexts));
722
- surface.append(renderModels(doc, catalog, viewState, callbacks, recordContexts));
717
+ const fold = (title, help, content, initialOpen) => {
718
+ const key = title === '哲学' ? 'philosophyOpen' : 'modelsOpen';
719
+ return workspaceDisclosure(doc, { title, help, content, open: viewState[key] ?? initialOpen, onToggle: open => { viewState[key] = open; } });
720
+ };
721
+ surface.append(fold('哲学', '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。', renderPhilosophies(doc, catalog, recordContexts), true));
722
+ surface.append(fold('世界モデル', '何をすると結果が変わるか、という仮説。未検証・許可用途を分けて確認します。', renderModels(doc, catalog, viewState, callbacks, recordContexts), false));
723
723
  root.append(surface);
724
724
  return surface;
725
725
  }
@@ -731,7 +731,7 @@ function appendRecordSource(doc, card, record, contexts) {
731
731
 
732
732
  /** Compose independently validated catalogs by topic, preserving each reference's source context. */
733
733
  export function renderFoundationCollectionOverview(root, sources, {
734
- document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体',
734
+ document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体', onNotice,
735
735
  } = {}) {
736
736
  const doc = getDocument(explicitDocument);
737
737
  root.replaceChildren();
@@ -741,14 +741,12 @@ export function renderFoundationCollectionOverview(root, sources, {
741
741
  let ready = 0;
742
742
  for (const source of sources) {
743
743
  if (source.state.status !== 'ready') {
744
- const notice = workspaceNotice(doc, {
745
- label: source.label,
746
- text: source.state.status === 'loading' ? '目的・哲学・世界モデルを読み込んでいます。'
747
- : `取得失敗: ${source.state.error?.message ?? 'この対象は未確認です。0件ではありません。'}`,
748
- tone: source.state.status === 'loading' ? 'info' : 'danger',
749
- });
750
- notice.append(workspaceButton(doc, { text: '再試行', variant: 'quiet', onClick: callbacks.load }));
751
- root.append(notice);
744
+ if (source.state.status === 'loading') {
745
+ root.append(workspaceNotice(doc, { label: source.label, text: '目的・哲学・世界モデルを読み込んでいます。' }));
746
+ } else {
747
+ const notice = { title: source.label, message: `取得失敗: ${source.state.error?.message ?? 'この対象は未確認です。0件ではありません。'}`, onRetry: callbacks.load };
748
+ if (onNotice) onNotice(notice); else root.append(workspaceErrorCard(doc, notice));
749
+ }
752
750
  continue;
753
751
  }
754
752
  const value = normalizeFoundationCatalog(source.state.catalog, source.scopeId);
@@ -780,7 +778,7 @@ export function renderFoundationCollectionOverview(root, sources, {
780
778
  const mount = makeElement(doc, 'div');
781
779
  renderFoundationOverview(mount, { status: 'ready', catalog }, {
782
780
  document: doc, scopeId: scopeLabel, showHeader: false, viewState, callbacks,
783
- recordContexts: contexts, partial: ready !== sources.length,
781
+ recordContexts: contexts, partial: ready !== sources.length, onNotice,
784
782
  });
785
783
  root.append(mount);
786
784
  }
@@ -813,6 +811,7 @@ export function createFoundationOverview({ root, port, scopeId, document: explic
813
811
  },
814
812
  async load() {
815
813
  if (destroyed) return state;
814
+ viewState.dialog?.close();
816
815
  const requestGeneration = ++generation;
817
816
  state.status = 'loading';
818
817
  state.catalog = null;
@@ -841,6 +840,7 @@ export function createFoundationOverview({ root, port, scopeId, document: explic
841
840
  destroy() {
842
841
  destroyed = true;
843
842
  generation += 1;
843
+ viewState.dialog?.close();
844
844
  root.replaceChildren();
845
845
  },
846
846
  };
@@ -282,11 +282,15 @@ export function createOwnObjectivesView({
282
282
  basePath = '/api/graph/own',
283
283
  label = GRAPH_OWN_OBJECTIVES_LABEL,
284
284
  autoLoad = true,
285
+ compact = false,
286
+ onState,
287
+ onNotice,
285
288
  } = {}) {
286
289
  if (!root) throw new TypeError('root is required');
287
290
  const doc = explicitDocument ?? globalThis.document;
288
291
  const source = graphOwnShareSource({ basePath, label }, { fetcher });
289
292
  const state = { list: { state: 'loading' }, selectedId: null };
293
+ let destroyed = false, generation = 0;
290
294
 
291
295
  function detail(objective) {
292
296
  const wrap = makeElement(doc, 'div', { className: 'bb-graph-own-detail', attrs: { 'aria-label': '選んだ目的(引き継いだ自分の分)' } });
@@ -316,7 +320,17 @@ export function createOwnObjectivesView({
316
320
 
317
321
  function body() {
318
322
  const notice = ownShareStateNotice(doc, state.list, { onRetry: () => controller.load(), loadingText: '引き継いだ自分の目的を読み込んでいます。' });
319
- if (notice) return [notice];
323
+ if (notice) {
324
+ if (compact && state.list.state !== 'loading' && state.list.state !== 'not_handed_over') {
325
+ onNotice?.({ title: label, message: `${state.list.reason ?? '引き継いだ自分の目的を取得できません。'} 0件ではありません。`, onRetry: () => controller.load() });
326
+ return [makeElement(doc, 'p', { className: 'bb-ws-inline-error', text: '取得できません' })];
327
+ }
328
+ if (compact && state.list.state === 'not_handed_over') {
329
+ const guide = makeElement(doc, 'details'); guide.append(makeElement(doc, 'summary', { text: '引き継ぎの案内' }), notice);
330
+ return [makeElement(doc, 'p', { text: 'まだ引き継いだ目的はありません。' }), guide];
331
+ }
332
+ return [notice];
333
+ }
320
334
  const { payload } = state.list;
321
335
  if (payload.objectives.length === 0) {
322
336
  return [makeElement(doc, 'p', { className: 'bb-graph-empty', text: payload.foundation === 'none' ? 'この束には、目的の定義がありません。' : 'この束には、目的がありません。' })];
@@ -346,18 +360,31 @@ export function createOwnObjectivesView({
346
360
  const controller = {
347
361
  get state() { return state; },
348
362
  render() {
363
+ if (destroyed) return controller;
349
364
  const handover = state.list.state === 'ok' ? state.list.payload.handover : null;
350
- root.replaceChildren(ownShareSection(doc, { label: source.label, lead: OBJECTIVES_LEAD, handover, children: body(), className: 'bb-graph-own-objectives' }));
365
+ if (compact) {
366
+ const facts = makeElement(doc, 'p', { className: 'bb-graph-own-facts', text: '読み取りのみ' });
367
+ const refs = makeElement(doc, 'details');
368
+ refs.append(makeElement(doc, 'summary', { text: '出典' }), makeElement(doc, 'p', { text: ownShareHandoverText(handover) ?? (state.list.state === 'not_handed_over' ? 'まだ引き継いでいません。' : '出典は確認できません。') }));
369
+ root.replaceChildren(facts, ...body(), refs);
370
+ }
371
+ else root.replaceChildren(ownShareSection(doc, { label: source.label, lead: OBJECTIVES_LEAD, handover, children: body(), className: 'bb-graph-own-objectives' }));
372
+ onState?.(state.list);
351
373
  return controller;
352
374
  },
353
375
  async load() {
376
+ if (destroyed) return state.list;
377
+ const requestGeneration = ++generation;
354
378
  state.list = { state: 'loading' };
355
379
  controller.render();
356
- state.list = normalizeOwnObjectives(await source.client.read('/objectives'));
380
+ const result = await source.client.read('/objectives');
381
+ if (destroyed || requestGeneration !== generation) return state.list;
382
+ state.list = normalizeOwnObjectives(result);
357
383
  if (state.list.state !== 'ok' || !state.list.payload.objectives.some((objective) => objective.id === state.selectedId)) state.selectedId = null;
358
384
  controller.render();
359
385
  return state.list;
360
386
  },
387
+ destroy() { destroyed = true; generation++; root.replaceChildren(); },
361
388
  select(id) {
362
389
  state.selectedId = textOrNull(id);
363
390
  return controller.render();
@@ -1,3 +1,4 @@
1
+ @import './native-scrollbars.css';
1
2
  /*
2
3
  * Shell of the local Web host, in the organization edition's layout: a white
3
4
  * left rail (brand, navigation, source) and a paper workspace. Uses only the
@@ -26,6 +26,7 @@ import { createMeetingMinutesStorageExtension } from './meeting-minutes-storage.
26
26
  import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-http.js';
27
27
  import { createWorldModelView } from './world-model-view.js';
28
28
  import { createGraphClient } from './graph-view-shared.js';
29
+ import { installNativeScrollbars } from './native-scrollbars.js';
29
30
 
30
31
  export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
31
32
 
@@ -459,6 +460,7 @@ export function createLocalWebShell({
459
460
  }
460
461
  shell.append(sidebar, backdrop, workspace, railAside);
461
462
  root.replaceChildren(shell);
463
+ const stopScrollbars = doc.documentElement?.classList && doc.addEventListener ? installNativeScrollbars(doc) : () => {};
462
464
 
463
465
  // The rail column shows only while the active screen is mounted and has one.
464
466
  function updateRail() {
@@ -493,6 +495,7 @@ export function createLocalWebShell({
493
495
  }
494
496
 
495
497
  const controller = {
498
+ destroy() { stopScrollbars(); for (const view of mounted.values()) view?.destroy?.(); root.replaceChildren(); },
496
499
  get state() {
497
500
  return { active, status, mounted: [...mounted.keys()] };
498
501
  },
@@ -0,0 +1,18 @@
1
+
2
+ @property --bb-scrollbar-opacity { syntax: "<number>"; inherits: false; initial-value: 0; }
3
+ :root.bb-native-scrollbars, .bb-native-scrollbars :is(.bb-scrollable, .sidebar, .bb-shell-sidebar, .bb-shell-main, .bb-shell-rail, .bb-ws-dialog, .mutation-dialog) {
4
+ --bb-scrollbar-rgb: var(--bb-color-scrollbar-rgb); --bb-scrollbar-opacity: 0;
5
+ scrollbar-width: thin;
6
+ scrollbar-color: rgba(var(--bb-scrollbar-rgb),var(--bb-scrollbar-opacity)) transparent;
7
+ transition: --bb-scrollbar-opacity 450ms ease;
8
+ }
9
+ .bb-native-scrollbars :is(.sidebar,.bb-shell-sidebar) { --bb-scrollbar-rgb: var(--bb-color-scrollbar-dark-rgb); }
10
+ :root.bb-native-scrollbars.bb-is-scrolling, .bb-native-scrollbars .bb-is-scrolling { --bb-scrollbar-opacity: .28; }
11
+ :root.bb-native-scrollbars::-webkit-scrollbar, .bb-native-scrollbars :is(.bb-scrollable,.sidebar,.bb-shell-sidebar,.bb-shell-main,.bb-shell-rail,.bb-ws-dialog,.mutation-dialog)::-webkit-scrollbar { width: 6px; height: 6px; }
12
+ :root.bb-native-scrollbars::-webkit-scrollbar-track, .bb-native-scrollbars :is(.bb-scrollable,.sidebar,.bb-shell-sidebar,.bb-shell-main,.bb-shell-rail,.bb-ws-dialog,.mutation-dialog)::-webkit-scrollbar-track { background: transparent; }
13
+ :root.bb-native-scrollbars::-webkit-scrollbar-thumb, .bb-native-scrollbars :is(.bb-scrollable,.sidebar,.bb-shell-sidebar,.bb-shell-main,.bb-shell-rail,.bb-ws-dialog,.mutation-dialog)::-webkit-scrollbar-thumb { background: rgba(var(--bb-scrollbar-rgb),var(--bb-scrollbar-opacity)); border-radius: 8px; }
14
+ @media (prefers-reduced-motion: reduce) { :root.bb-native-scrollbars, .bb-native-scrollbars :is(.bb-scrollable,.sidebar,.bb-shell-sidebar,.bb-shell-main,.bb-shell-rail,.bb-ws-dialog,.mutation-dialog) { transition: none; } }
15
+ @media (forced-colors: active) {
16
+ :root.bb-native-scrollbars, .bb-native-scrollbars :is(.bb-scrollable,.sidebar,.bb-shell-sidebar,.bb-shell-main,.bb-shell-rail,.bb-ws-dialog,.mutation-dialog) { scrollbar-width: auto; scrollbar-color: auto; }
17
+ :root.bb-native-scrollbars::-webkit-scrollbar-thumb, .bb-native-scrollbars :is(.bb-scrollable,.sidebar,.bb-shell-sidebar,.bb-shell-main,.bb-shell-rail,.bb-ws-dialog,.mutation-dialog)::-webkit-scrollbar-thumb { background: CanvasText; }
18
+ }
@@ -0,0 +1,18 @@
1
+ /** Animate native scrollbars without replacing scrolling or input handling. */
2
+ export function installNativeScrollbars(doc = document) {
3
+ const timers = new Map();
4
+ doc.documentElement.classList.add('bb-native-scrollbars');
5
+ const activate = event => {
6
+ const target = event.target === doc ? doc.scrollingElement : event.target;
7
+ if (!target?.classList) return;
8
+ target.classList.add('bb-scrollable', 'bb-is-scrolling');
9
+ clearTimeout(timers.get(target));
10
+ timers.set(target, setTimeout(() => { target.classList.remove('bb-is-scrolling'); timers.delete(target); }, 800));
11
+ };
12
+ doc.addEventListener('scroll', activate, true);
13
+ return () => {
14
+ doc.removeEventListener('scroll', activate, true);
15
+ for (const [target, timer] of timers) { clearTimeout(timer); target.classList.remove('bb-is-scrolling'); }
16
+ timers.clear(); doc.documentElement.classList.remove('bb-native-scrollbars');
17
+ };
18
+ }
@@ -494,3 +494,39 @@
494
494
  }
495
495
 
496
496
  }
497
+
498
+ /* Shared help, modal, disclosures and persistent notifications. */
499
+ .bb-ws-help { position: relative; display: inline-flex; vertical-align: middle; }
500
+ .bb-ws-help-button { display: grid; place-items: center; width: 16px; height: 16px; padding: 0; border: 1px solid var(--bb-color-muted); border-radius: 50%; background: transparent; color: var(--bb-color-muted); font: italic 11px var(--bb-font-help); cursor: help; }
501
+ .bb-ws-help-bubble { position: absolute; z-index: 70; bottom: calc(100% + 8px); left: 50%; width: min(300px,70vw); padding: 10px 12px; border: 1px solid var(--bb-color-border); border-radius: 8px; background: var(--bb-color-panel); color: var(--bb-color-ink); font-size: 12px; font-weight: normal; line-height: 1.7; box-shadow: var(--bb-shadow-help); opacity: 0; visibility: hidden; transform: translate(-50%,4px); transition: opacity 160ms ease,transform 160ms ease,visibility 160ms; pointer-events: none; }
502
+ .bb-ws-help-bubble::after { content: ''; position: absolute; top: 100%; left: 0; width: 100%; height: 10px; }
503
+ .bb-ws-help:not(.is-dismissed):is(:hover,:focus-within,.is-open) .bb-ws-help-bubble { opacity: 1; visibility: visible; transform: translate(-50%,0); pointer-events: auto; }
504
+ .bb-ws-dialog { width: min(720px,calc(100vw - 36px)); max-height: calc(100dvh - 48px); box-sizing: border-box; padding: 0; overflow: auto; border: 1px solid var(--bb-color-border); border-radius: 12px; background: var(--bb-color-panel); color: var(--bb-color-ink); font: inherit; }
505
+ .bb-ws-dialog::backdrop { background: var(--bb-color-dialog-overlay); }
506
+ .bb-ws-dialog-head { display: flex; justify-content: space-between; align-items: start; gap: 20px; padding: 24px 28px; border-bottom: 1px solid var(--bb-color-border); }
507
+ .bb-ws-dialog-head h2 { margin: 0; font-size: 22px; line-height: 1.5; }
508
+ .bb-ws-dialog-body { padding: 28px; }
509
+ .bb-ws-disclosure { border-top: 1px solid var(--bb-color-border); }
510
+ .bb-ws-disclosure > summary { display: flex; align-items: center; gap: 12px; padding: 16px 0; cursor: pointer; list-style: none; }
511
+ .bb-ws-disclosure > summary::-webkit-details-marker { display: none; }
512
+ .bb-ws-disclosure > summary::before { content: '›'; font-size: 20px; color: var(--bb-color-muted); }
513
+ .bb-ws-disclosure[open] > summary::before { transform: rotate(90deg); }
514
+ .bb-ws-disclosure > summary h2 { margin: 0; font-size: 22px; line-height: 1.4; }
515
+ .bb-ws-disclosure > :not(summary) { margin: 0 0 18px 25px; }
516
+ .bb-ws-disclosure > summary:focus-visible, .bb-ws-help-button:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 3px; }
517
+ .bb-ws-error-stack { position: absolute; z-index: 55; top: 0; right: 0; width: min(300px,100%); max-height: none; padding-bottom: 64px; pointer-events: none; box-sizing: border-box; overflow: visible; }
518
+ .bb-ws-error-list { display: grid; gap: 6px; }
519
+ .bb-ws-error-count { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
520
+ .bb-ws-error-card { display: flex; align-items: start; gap: 8px; box-sizing: border-box; width: 100%; padding: 8px 10px; border: 1px solid var(--bb-color-notice-border); border-left: 3px solid var(--bb-color-notice-danger); border-radius: 4px; background: var(--bb-color-notice-background); backdrop-filter: blur(5px); color: var(--bb-color-notice-text); font-size: 12px; line-height: 1.5; pointer-events: auto; }
521
+ .bb-ws-error-card.is-warning { border-left-color: var(--bb-color-notice-warning); }
522
+ .bb-ws-error-card > details { flex: 1; min-width: 0; }
523
+ .bb-ws-error-card summary { display: flex; align-items: center; gap: 6px; list-style: none; cursor: pointer; }
524
+ .bb-ws-error-card summary::-webkit-details-marker { display: none; }
525
+ .bb-ws-error-card summary > span:last-child { overflow-wrap: anywhere; }
526
+ .bb-ws-error-symbol { display: grid; flex: none; place-items: center; width: 14px; height: 14px; border: 1px solid var(--bb-color-notice-danger-symbol-border); color: var(--bb-color-notice-danger-text); border-radius: 3px; }
527
+ .bb-ws-error-card.is-warning .bb-ws-error-symbol { color: var(--bb-color-notice-warning-text); border-color: var(--bb-color-notice-warning-symbol-border); }
528
+ .bb-ws-error-card p { margin: 8px 0 0; overflow-wrap: anywhere; font-size: 11px; }
529
+ .bb-ws-error-card .bb-ws-button { flex: none; min-height: 18px; padding: 0 5px; border: 1px solid var(--bb-color-notice-control-border); border-radius: 4px; background: transparent; color: var(--bb-color-notice-text); font-size: 11px; line-height: 16px; }
530
+ .bb-ws-error-card :is(summary,button):focus-visible { outline: 2px solid var(--bb-color-notice-text); outline-offset: 2px; }
531
+ .bb-ws-inline-error { height: 15px; max-height: 15px; margin: 4px 0; padding: 0; overflow: hidden; color: var(--bb-color-inline-error); font-size: 11px; line-height: 15px; }
532
+ @media (prefers-reduced-motion: reduce) { .bb-ws-help-bubble { transition: none; } }
@@ -230,3 +230,68 @@ export function workspaceSectionTitle(doc, { title, lead } = {}) {
230
230
  if (lead) wrap.append(el(doc, 'p', { text: lead }));
231
231
  return wrap;
232
232
  }
233
+
234
+
235
+ let workspaceSequence = 0;
236
+ /** Shared explanatory help: hover/focus above the control, click for touch. */
237
+ export function workspaceHelp(doc, { text, label } = {}) {
238
+ const wrap = el(doc, 'span', { className: 'bb-ws-help' });
239
+ const control = workspaceButton(doc, { text: 'i', attrs: { 'aria-label': `${label}の説明`, 'aria-expanded': 'false' } });
240
+ control.className = 'bb-ws-help-button';
241
+ const bubble = el(doc, 'span', { className: 'bb-ws-help-bubble', text, attrs: { id: `bb-ws-help-${++workspaceSequence}`, role: 'tooltip' } });
242
+ control.setAttribute('aria-describedby', bubble.id || bubble.getAttribute?.('id') || bubble.attributes?.id);
243
+ control.addEventListener('click', event => {
244
+ event.stopPropagation(); event.preventDefault();
245
+ const opened = wrap.classList.toggle('is-open');
246
+ wrap.classList.remove('is-dismissed');
247
+ control.setAttribute('aria-expanded', String(opened));
248
+ });
249
+ wrap.addEventListener('mouseleave', () => wrap.classList.remove('is-dismissed'));
250
+ control.addEventListener('blur', () => { wrap.classList.remove('is-open', 'is-dismissed'); control.setAttribute('aria-expanded', 'false'); });
251
+ control.addEventListener('keydown', event => {
252
+ if (event.key !== 'Escape') return;
253
+ wrap.classList.remove('is-open'); wrap.classList.add('is-dismissed');
254
+ control.setAttribute('aria-expanded', 'false'); event.stopPropagation();
255
+ });
256
+ wrap.append(control, bubble); return wrap;
257
+ }
258
+
259
+ /** Native dialog keeps focus trapped, Escape close and focus restoration. */
260
+ export function workspaceDialog(doc, { title, content, opener } = {}) {
261
+ const dialog = el(doc, 'dialog', { className: 'bb-ws-dialog' });
262
+ const headingId = `bb-ws-dialog-${++workspaceSequence}`;
263
+ dialog.setAttribute('aria-labelledby', headingId);
264
+ const head = el(doc, 'header', { className: 'bb-ws-dialog-head' });
265
+ const close = workspaceButton(doc, { text: '×', attrs: { 'aria-label': '閉じる' }, onClick: () => dialog.close() });
266
+ head.append(el(doc, 'h2', { text: title, attrs: { id: headingId } }), close);
267
+ const body = el(doc, 'div', { className: 'bb-ws-dialog-body' }); appendContent(body, doc, content);
268
+ dialog.append(head, body); doc.body.append(dialog);
269
+ dialog.addEventListener('click', event => {
270
+ if (event.target !== dialog) return;
271
+ const rect = dialog.getBoundingClientRect();
272
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
273
+ });
274
+ dialog.addEventListener('close', () => { dialog.remove(); if (opener?.isConnected) opener.focus(); }, { once: true });
275
+ dialog.showModal(); close.focus(); return dialog;
276
+ }
277
+
278
+ /** Persistent, initially closed notification. The caller owns its lifecycle. */
279
+ export function workspaceErrorCard(doc, { title, message, tone = 'danger', onRetry } = {}) {
280
+ const card = el(doc, 'section', { className: `bb-ws-error-card is-${tone}`, attrs: { role: 'listitem' } });
281
+ const details = el(doc, 'details');
282
+ const summary = el(doc, 'summary', { attrs: { 'aria-label': `${title}の通知` } });
283
+ summary.append(el(doc, 'span', { className: 'bb-ws-error-symbol', text: '!', attrs: { 'aria-hidden': 'true' } }), el(doc, 'span', { text: title }));
284
+ details.append(summary, el(doc, 'p', { text: message })); card.append(details);
285
+ if (onRetry) card.append(workspaceButton(doc, { text: '再試行', attrs: { 'aria-label': `${title}を再試行` }, onClick: onRetry }));
286
+ return card;
287
+ }
288
+
289
+ export function workspaceDisclosure(doc, { title, help, content, open = false, onToggle } = {}) {
290
+ const details = el(doc, 'details', { className: 'bb-ws-disclosure' }); details.open = open;
291
+ const summary = el(doc, 'summary');
292
+ summary.append(el(doc, 'h2', { text: title }));
293
+ if (help) summary.append(workspaceHelp(doc, { label: title, text: help }));
294
+ details.append(summary, content);
295
+ if (onToggle) details.addEventListener('toggle', () => onToggle(details.open));
296
+ return details;
297
+ }