@unson/brainbase-mcp 0.41.4 → 0.41.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.4",
3
+ "version": "0.41.7",
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",
@@ -483,5 +483,5 @@
483
483
  "engines": {
484
484
  "node": ">=20"
485
485
  },
486
- "gitHead": "56d428ab1e2ba87b273a09119b2c5b4f25314440"
486
+ "gitHead": "218515bb85c16915c1e509f608f83aa06af3a5d1"
487
487
  }
@@ -26,6 +26,8 @@
26
26
  --bb-color-accent: #5b4bd8;
27
27
  --bb-color-accent-soft: #efedff;
28
28
  --bb-color-accent-strong: #4939b8;
29
+ /* Minutes screen remaps its accent to this indigo. Shared accent stays purple. */
30
+ --bb-color-brand-indigo: #0F2A43;
29
31
  --bb-color-action: #5b4bd8;
30
32
  --bb-color-action-hover: #4939b8;
31
33
  --bb-color-action-text: #ffffff;
@@ -62,6 +64,10 @@
62
64
  --bb-color-notice-control-border: #ffffff45;
63
65
  --bb-color-inline-error: #9b3d3d;
64
66
  --bb-color-record-missing: #80601c;
67
+ --bb-color-drawer-overlay: rgb(13 25 46 / 6%);
68
+ --bb-color-drawer-overlay-clear: rgb(13 25 46 / 0%);
69
+ --bb-shadow-drawer: -18px 0 48px rgb(13 25 46 / 10%);
70
+ --bb-shadow-popover: 0 8px 28px rgb(13 25 46 / 14%);
65
71
  --bb-color-dialog-overlay: #0d192e66;
66
72
  --bb-color-scrollbar-rgb: 36,50,75;
67
73
  --bb-color-scrollbar-dark-rgb: 203,212,225;
@@ -14,7 +14,7 @@ import {
14
14
  workspaceNotice,
15
15
  workspacePageHeader,
16
16
  workspaceSectionTitle,
17
- workspaceHelp, workspaceDialog, workspaceDisclosure, workspaceErrorCard,
17
+ workspaceHelp, workspaceDialog, workspaceDisclosure, workspaceErrorCard, workspaceLoadingCard, workspaceDismissNotification,
18
18
  } from './workspace-kit.js';
19
19
 
20
20
  export const FOUNDATION_OVERVIEW_CONTRACT_VERSION = 'foundation-overview.v1';
@@ -660,10 +660,25 @@ function renderModels(doc, catalog, viewState, callbacks, recordContexts) {
660
660
  return section;
661
661
  }
662
662
 
663
- function renderLoading(doc, state, callbacks) {
664
- const notice = workspaceNotice(doc, { label: '読み込み', text: '目的・哲学・世界モデルを読み込んでいます。' });
665
- notice.append(workspaceButton(doc, { text: '再試行', variant: 'quiet', onClick: callbacks.load }));
666
- return notice;
663
+ /** Keep one loading card across redraws so completion can leave visibly. */
664
+ function renderLoadingState(doc, root, viewState, loading, onLoading) {
665
+ if (onLoading) { onLoading(loading); return; }
666
+ if (loading) {
667
+ viewState.cancelLoadingDismiss?.();
668
+ viewState.cancelLoadingDismiss = null;
669
+ viewState.loadingCard ??= workspaceLoadingCard(doc);
670
+ }
671
+ const card = viewState.loadingCard;
672
+ if (!card) return;
673
+ const stack = viewState.loadingStack ??= makeElement(doc, 'aside', { className: 'bb-ws-error-stack', attrs: { 'aria-label': '読み込み状況' } });
674
+ stack.append(card); root.append(stack);
675
+ if (!loading && !viewState.cancelLoadingDismiss) {
676
+ const cancel = workspaceDismissNotification(card, () => {
677
+ if (viewState.loadingCard !== card) return;
678
+ viewState.loadingCard = null; viewState.cancelLoadingDismiss = null; viewState.loadingStack = null; stack.remove();
679
+ });
680
+ if (viewState.loadingCard === card) viewState.cancelLoadingDismiss = cancel;
681
+ }
667
682
  }
668
683
 
669
684
  function renderFailure(doc, state, callbacks) {
@@ -684,6 +699,7 @@ export function renderFoundationOverview(root, state, {
684
699
  recordContexts,
685
700
  partial = false,
686
701
  onNotice,
702
+ onLoading,
687
703
  } = {}) {
688
704
  if (!root) throw new TypeError('root is required');
689
705
  const doc = getDocument(explicitDocument);
@@ -705,14 +721,13 @@ export function renderFoundationOverview(root, state, {
705
721
  else surface.append(workspaceErrorCard(doc, notice));
706
722
  };
707
723
  if (state.status === 'loading') {
708
- surface.append(renderLoading(doc, state, callbacks)); root.append(surface); return surface;
724
+ root.append(surface); renderLoadingState(doc, root, viewState, true, onLoading); return surface;
709
725
  }
710
726
  if (state.status !== 'ready') {
711
727
  notify({ title: '取得失敗', message: `${state.error?.message ?? '目的・哲学・世界モデルを確認できません。'} 登録なし・0件として表示していません。`, onRetry: callbacks.load });
712
- root.append(surface); return surface;
728
+ root.append(surface); renderLoadingState(doc, root, viewState, false, onLoading); return surface;
713
729
  }
714
730
  const catalog = state.catalog;
715
- if (partial) notify({ title: '取得済みの範囲', tone: 'warning', message: '以下の登録なし・検索結果は取得できた記録だけが対象です。全体は未確認です。' });
716
731
  surface.append(renderObjectives(doc, catalog, viewState, callbacks, recordContexts));
717
732
  const fold = (title, help, content, initialOpen) => {
718
733
  const key = title === '哲学' ? 'philosophyOpen' : 'modelsOpen';
@@ -721,6 +736,7 @@ export function renderFoundationOverview(root, state, {
721
736
  surface.append(fold('哲学', '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。', renderPhilosophies(doc, catalog, recordContexts), true));
722
737
  surface.append(fold('世界モデル', '何をすると結果が変わるか、という仮説。未検証・許可用途を分けて確認します。', renderModels(doc, catalog, viewState, callbacks, recordContexts), false));
723
738
  root.append(surface);
739
+ renderLoadingState(doc, root, viewState, false, onLoading);
724
740
  return surface;
725
741
  }
726
742
 
@@ -731,7 +747,7 @@ function appendRecordSource(doc, card, record, contexts) {
731
747
 
732
748
  /** Compose independently validated catalogs by topic, preserving each reference's source context. */
733
749
  export function renderFoundationCollectionOverview(root, sources, {
734
- document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体', onNotice,
750
+ document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体', onNotice, onLoading,
735
751
  } = {}) {
736
752
  const doc = getDocument(explicitDocument);
737
753
  root.replaceChildren();
@@ -742,7 +758,7 @@ export function renderFoundationCollectionOverview(root, sources, {
742
758
  for (const source of sources) {
743
759
  if (source.state.status !== 'ready') {
744
760
  if (source.state.status === 'loading') {
745
- root.append(workspaceNotice(doc, { label: source.label, text: '目的・哲学・世界モデルを読み込んでいます。' }));
761
+ // A single shared loading card is rendered after the available catalogs.
746
762
  } else {
747
763
  const notice = { title: source.label, message: `取得失敗: ${source.state.error?.message ?? 'この対象は未確認です。0件ではありません。'}`, onRetry: callbacks.load };
748
764
  if (onNotice) onNotice(notice); else root.append(workspaceErrorCard(doc, notice));
@@ -778,10 +794,11 @@ export function renderFoundationCollectionOverview(root, sources, {
778
794
  const mount = makeElement(doc, 'div');
779
795
  renderFoundationOverview(mount, { status: 'ready', catalog }, {
780
796
  document: doc, scopeId: scopeLabel, showHeader: false, viewState, callbacks,
781
- recordContexts: contexts, partial: ready !== sources.length, onNotice,
797
+ recordContexts: contexts, partial: ready !== sources.length, onNotice, onLoading: () => {},
782
798
  });
783
799
  root.append(mount);
784
800
  }
801
+ renderLoadingState(doc, root, viewState, sources.some(source => source.state.status === 'loading'), onLoading);
785
802
  return root;
786
803
  }
787
804
 
@@ -841,6 +858,8 @@ export function createFoundationOverview({ root, port, scopeId, document: explic
841
858
  destroyed = true;
842
859
  generation += 1;
843
860
  viewState.dialog?.close();
861
+ viewState.cancelLoadingDismiss?.();
862
+ viewState.loadingCard = null;
844
863
  root.replaceChildren();
845
864
  },
846
865
  };
@@ -583,3 +583,37 @@
583
583
  .bb-jh-ledger .bb-ws-ledger-row > span:nth-child(3)::before { content: '使った参照'; }
584
584
  .bb-jh-ledger .bb-ws-ledger-row > span:nth-child(4)::before { content: '入口・状態'; }
585
585
  }
586
+
587
+ /* Compact workspace header, search controls and shared floating notifications. */
588
+ .bb-jh { position: relative; }
589
+ .bb-jh > .bb-ws-page { margin-bottom: var(--bb-space-6, 32px); }
590
+ .bb-jh .bb-ws-page-head h1 { font-size: 24px; line-height: 1.4; font-weight: 600; letter-spacing: .02em; }
591
+ .bb-jh-controls { margin-bottom: var(--bb-space-4, 16px); }
592
+ .bb-jh-checked { font-size: 11px; font-variant-numeric: tabular-nums; }
593
+ .bb-jh-filter-fields { display: grid; gap: 16px; }
594
+ .bb-jh-filter-fields .bb-jh-periods { flex-wrap: wrap; gap: 6px; }
595
+ .bb-jh-filter-fields .bb-jh-normal-filters { display: grid; flex: none; gap: 16px; }
596
+ .bb-jh-filter-fields .bb-jh-filter { display: grid; gap: 6px; }
597
+ .bb-jh-filter-fields select { width: 100%; }
598
+ .bb-jh-refresh { display: grid; place-items: center; flex: none; width: 32px; height: 32px; padding: 6px; }
599
+ .bb-jh-refresh-mark { display: block; width: 16px; height: 16px; }
600
+ .bb-jh-notifications { display: grid; gap: 8px; }
601
+ .bb-jh-notifications .bb-jh-banner { margin: 0; }
602
+ .bb-jh-notifications .bb-jh-banner-sources { display: grid; gap: 4px; margin-top: 8px; }
603
+ .bb-jh-notifications .bb-jh-source-detail { margin: 8px 0 0; }
604
+ .bb-jh-notifications .bb-jh-source-detail summary { color: inherit; }
605
+ .bb-jh-notifications .bb-jh-source-detail-body { position: static; max-width: none; background: transparent; border-color: var(--bb-color-notice-border); color: inherit; }
606
+ .bb-jh-detail .bb-ws-rail-head { margin-top: 0; }
607
+ @media (max-width: 760px) {
608
+ .bb-jh-controls { flex-direction: row; flex-wrap: wrap; align-items: center; }
609
+ .bb-jh-search { flex: 1 1 150px; }
610
+ .bb-jh-toolbar-status { margin-left: auto; }
611
+ }
612
+ .bb-jh-notifications .bb-jh-banner {
613
+ border-color: var(--bb-color-notice-border);
614
+ background: var(--bb-color-notice-background);
615
+ color: var(--bb-color-notice-text);
616
+ border-left-color: var(--bb-color-notice-warning);
617
+ }
618
+ .bb-jh-notifications .bb-jh-banner.is-danger { border-left-color: var(--bb-color-notice-danger); }
619
+ .bb-jh-notifications .bb-jh-banner-sources > span { white-space: normal; }
@@ -11,6 +11,11 @@
11
11
  import {
12
12
  makeWorkspaceElement,
13
13
  workspaceButton,
14
+ workspaceDialog,
15
+ workspaceFilterPopover,
16
+ workspaceErrorCard,
17
+ workspaceLoadingCard,
18
+ workspaceDismissNotification,
14
19
  workspaceDefinition,
15
20
  workspaceDetailEmpty,
16
21
  workspaceLedger,
@@ -1278,14 +1283,16 @@ function sourceChips(doc, sources) {
1278
1283
  }
1279
1284
 
1280
1285
  function historyBanner(doc, { label, tone, role, sources, home, note, reload }) {
1281
- const banner = makeWorkspaceElement(doc, 'div', {
1282
- className: `bb-jh-banner is-${tone}`,
1283
- attrs: { role: role ?? 'status' },
1286
+ const banner = workspaceErrorCard(doc, {
1287
+ title: label,
1288
+ tone,
1289
+ message: note || '取得できた記録だけを表示しています。全体の取得状態は通知の詳細で確認できます。',
1284
1290
  });
1285
- banner.append(makeWorkspaceElement(doc, 'strong', { text: label }));
1286
- if (sources?.length) banner.append(sourceChips(doc, sources));
1287
- if (note) banner.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-banner-note', text: note }));
1288
- if (home) banner.append(sourceDetail(doc, home));
1291
+ banner.className += ` bb-jh-banner is-${tone}`;
1292
+ banner.setAttribute('role', role ?? 'status');
1293
+ const details = findFirst(banner, node => node.tagName === 'DETAILS');
1294
+ if (sources?.length) details.append(sourceChips(doc, sources));
1295
+ if (home) details.append(sourceDetail(doc, home));
1289
1296
  if (reload) banner.append(reload);
1290
1297
  return banner;
1291
1298
  }
@@ -1315,30 +1322,7 @@ function statusNotice(doc, phase, error, home, onRetry, { loading = false, slow
1315
1322
  });
1316
1323
  }
1317
1324
  if (loading) {
1318
- const progressHome = arrived ?? home;
1319
- const counts = new Map();
1320
- for (const record of progressHome?.records ?? []) {
1321
- const key = normalEntrypoint(record.entrypoint);
1322
- counts.set(key, (counts.get(key) ?? 0) + 1);
1323
- }
1324
- const known = Array.isArray(progressHome?.coverage?.sources) && progressHome.coverage.sources.length
1325
- ? progressHome.coverage.sources
1326
- : NORMAL_ENTRYPOINTS.filter((entrypoint) => entrypoint !== 'unknown').map((entrypoint) => ({ entrypoint, status: 'pending', reason: null }));
1327
- const sources = known.map((source) => {
1328
- const count = counts.get(normalEntrypoint(source.entrypoint)) ?? 0;
1329
- if (count > 0) return { entrypoint: source.entrypoint, status: 'partial', retrieved: count, reason: null };
1330
- if (source.status === 'unavailable') return { entrypoint: source.entrypoint, status: 'unavailable', reason: source.reason ?? null };
1331
- return { entrypoint: source.entrypoint, status: 'pending', reason: null };
1332
- });
1333
- return historyBanner(doc, {
1334
- label: '読み込み中',
1335
- tone: 'info',
1336
- role: 'status',
1337
- sources,
1338
- note: slow
1339
- ? '時間がかかっています。届いた分だけ先に見られます。'
1340
- : '取得元ごとに届いた順に表示します',
1341
- });
1325
+ return workspaceLoadingCard(doc, { title: 'ロード中' });
1342
1326
  }
1343
1327
  if (home?.status === 'partial') {
1344
1328
  return historyBanner(doc, {
@@ -1522,12 +1506,6 @@ function summaryLine(doc, aggregate, { loading = false } = {}) {
1522
1506
  line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1523
1507
  line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1524
1508
  }
1525
- if (!complete) {
1526
- line.append(makeWorkspaceElement(doc, 'span', {
1527
- className: 'bb-jh-summary-note',
1528
- text: loading ? '(読み込み中・全件数は未確認)' : '(全件数は未確認)',
1529
- }));
1530
- }
1531
1509
  const references = loading ? null : (aggregate?.stats?.referenceCount ?? aggregate?.stats?.references);
1532
1510
  const confirmed = loading ? null : (aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes);
1533
1511
  line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 使った参照 ' }));
@@ -1578,6 +1556,7 @@ export function createJudgmentHistoryUI({
1578
1556
  page,
1579
1557
  autoLoad = true,
1580
1558
  pageHeader = true,
1559
+ notificationRoot = null,
1581
1560
  now = new Date(),
1582
1561
  skeletonDelayMs = 300,
1583
1562
  slowDelayMs = 10000,
@@ -1621,6 +1600,52 @@ export function createJudgmentHistoryUI({
1621
1600
  slowTimer: null,
1622
1601
  };
1623
1602
 
1603
+ let filterPopover = null;
1604
+ let filterExpanded = false;
1605
+ let drawer = null;
1606
+ let drawerContent = null;
1607
+ let drawerKey = null;
1608
+ let suppressDrawerClose = false;
1609
+ function disposeDrawer() {
1610
+ suppressDrawerClose = true;
1611
+ drawer?.dispose();
1612
+ drawer = null; drawerContent = null; drawerKey = null;
1613
+ suppressDrawerClose = false;
1614
+ }
1615
+ const nativeDrawer = Boolean(doc.body && typeof doc.createElement('dialog').showModal === 'function');
1616
+ const previousRailHidden = rail?.hidden;
1617
+ if (nativeDrawer && rail) rail.hidden = true;
1618
+ const notifications = makeWorkspaceElement(doc, 'div', {
1619
+ className: 'bb-ws-error-stack bb-jh-notifications', attrs: { 'aria-label': '判断履歴の通知' },
1620
+ });
1621
+ if (notificationRoot) notificationRoot.append(notifications);
1622
+ let loadingCard = null;
1623
+ let dismissLoading = null;
1624
+ let statusCard = null;
1625
+ let statusKey = null;
1626
+
1627
+ function updateNotifications(loading) {
1628
+ if (loading) {
1629
+ dismissLoading?.(); dismissLoading = null;
1630
+ if (!loadingCard) loadingCard = workspaceLoadingCard(doc, { title: 'ロード中' });
1631
+ if (loadingCard.parentNode !== notifications) notifications.append(loadingCard);
1632
+ } else if (loadingCard && !dismissLoading) {
1633
+ dismissLoading = workspaceDismissNotification(loadingCard);
1634
+ }
1635
+ const home = state.home;
1636
+ const key = loading ? null : JSON.stringify([state.phase, state.error, home?.status, home?.coverage]);
1637
+ if (statusKey !== key) {
1638
+ statusCard?.remove?.();
1639
+ // Small host DOM adapters may not implement Element.remove().
1640
+ if (statusCard?.parentNode === notifications && typeof statusCard.remove !== 'function') {
1641
+ notifications.replaceChildren(...notifications.children.filter(node => node !== statusCard));
1642
+ }
1643
+ statusKey = key;
1644
+ statusCard = loading ? null : statusNotice(doc, state.phase, state.error, home, () => void load());
1645
+ if (statusCard) notifications.append(statusCard);
1646
+ }
1647
+ }
1648
+
1624
1649
  function clearLoadTimers() {
1625
1650
  if (state.skeletonTimer !== null) {
1626
1651
  clearTimeout(state.skeletonTimer);
@@ -1664,12 +1689,13 @@ export function createJudgmentHistoryUI({
1664
1689
  state.selectedKey = null;
1665
1690
  state.detailPhase = 'idle';
1666
1691
  state.detailError = null;
1692
+ if (drawer) drawer.close();
1667
1693
  render();
1668
1694
  return true;
1669
1695
  }
1670
1696
 
1671
1697
  function handleKeyDown(event) {
1672
- if (event?.key === 'Escape') closeSelected();
1698
+ if (event?.key === 'Escape' && !drawer) closeSelected();
1673
1699
  }
1674
1700
 
1675
1701
  root.addEventListener?.('keydown', handleKeyDown);
@@ -1908,7 +1934,18 @@ export function createJudgmentHistoryUI({
1908
1934
 
1909
1935
  function render() {
1910
1936
  if (state.disposed) return;
1911
- if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) return;
1937
+ updateNotifications(state.phase === 'loading');
1938
+ if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) {
1939
+ if (!notificationRoot && notifications.parentNode !== root) root.append(notifications);
1940
+ return;
1941
+ }
1942
+ const focusedFilter = filterExpanded && filterPopover
1943
+ ? findFirst(filterPopover.panel, node => node === doc.activeElement) : null;
1944
+ const filterFocus = focusedFilter && {
1945
+ label: attributeValue(focusedFilter, 'aria-label'), period: attributeValue(focusedFilter, 'data-period'),
1946
+ };
1947
+ filterPopover?.destroy();
1948
+ filterPopover = null;
1912
1949
  const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
1913
1950
  const previousSearchSelection = previousSearchInput && Number.isInteger(previousSearchInput.selectionStart)
1914
1951
  ? { start: previousSearchInput.selectionStart, end: previousSearchInput.selectionEnd ?? previousSearchInput.selectionStart }
@@ -1936,21 +1973,18 @@ export function createJudgmentHistoryUI({
1936
1973
  source: context.source,
1937
1974
  }));
1938
1975
  }
1939
- const notice = statusNotice(doc, state.phase, state.error, loading ? (state.arrived ?? state.home) : state.home, () => void load(), {
1940
- loading,
1941
- slow: state.slow,
1942
- arrived: state.arrived,
1943
- });
1944
- if (notice) wrapper.append(notice);
1976
+ if (!notificationRoot) wrapper.append(notifications);
1945
1977
  const failed = state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid';
1946
1978
  if (failed) {
1947
1979
  wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
1948
1980
  root.replaceChildren(wrapper);
1949
- renderRail(doc, rail, null, null);
1981
+ if (drawer) disposeDrawer();
1982
+ if (!nativeDrawer) renderRail(doc, rail, null, null);
1950
1983
  return;
1951
1984
  }
1952
1985
  const controls = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-controls' });
1953
- controls.append(periodButtons(doc, state.period, (period) => {
1986
+ const filterFields = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-filter-fields' });
1987
+ filterFields.append(periodButtons(doc, state.period, (period) => {
1954
1988
  state.period = period;
1955
1989
  state.selectedKey = null;
1956
1990
  state.cursor = null;
@@ -1961,7 +1995,7 @@ export function createJudgmentHistoryUI({
1961
1995
  }
1962
1996
  }));
1963
1997
  const showNormalFilters = loading ? sourceHome?.mode !== 'legacy' : normalMode();
1964
- if (showNormalFilters) controls.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
1998
+ if (showNormalFilters) filterFields.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
1965
1999
  controls.append(createSearch(doc, state.query, (query) => {
1966
2000
  state.query = String(query);
1967
2001
  state.focusSearch = true;
@@ -1970,28 +2004,41 @@ export function createJudgmentHistoryUI({
1970
2004
  if (searchable) state.aggregate = aggregateJudgmentHistory(searchable, { now: readNow(), period: state.period, query: state.query });
1971
2005
  render();
1972
2006
  }));
2007
+ filterPopover = workspaceFilterPopover(doc, {
2008
+ content: filterFields, open: filterExpanded,
2009
+ onToggle: value => { filterExpanded = value; },
2010
+ });
2011
+ controls.append(filterPopover.root);
1973
2012
  const toolbarStatus = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-toolbar-status' });
1974
- if (loading) {
1975
- toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
1976
- className: 'bb-jh-checked',
1977
- text: '確認しています…',
1978
- attrs: { role: 'status' },
1979
- }));
1980
- } else if (state.checkedAt) {
2013
+ if (state.checkedAt) {
1981
2014
  const stamp = formatCheckedAt(state.checkedAt);
1982
- if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-checked', text: `${stamp}に確認` }));
2015
+ if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
2016
+ className: 'bb-jh-checked', text: stamp, attrs: { 'aria-label': `${stamp}に確認` },
2017
+ }));
1983
2018
  }
1984
- toolbarStatus.append(workspaceButton(doc, {
1985
- text: '再読み込み',
1986
- variant: 'quiet',
1987
- disabled: loading,
1988
- onClick: () => void load(),
1989
- attrs: { 'aria-label': '判断履歴を再読み込み', 'data-action': 'reload' },
1990
- }));
2019
+ const reloadButton = workspaceButton(doc, {
2020
+ variant: 'quiet', disabled: loading, onClick: () => void load(),
2021
+ attrs: { 'aria-label': '判断履歴を再読み込み', title: '再読み込み', 'data-action': 'reload' },
2022
+ });
2023
+ reloadButton.className += ' bb-jh-refresh';
2024
+ // Text-independent vector keeps the refresh control available in standalone hosts.
2025
+ const refreshMark = doc.createElementNS?.('http://www.w3.org/2000/svg', 'svg')
2026
+ ?? makeWorkspaceElement(doc, 'span', { text: '↻' });
2027
+ refreshMark.setAttribute('class', 'bb-jh-refresh-mark'); refreshMark.setAttribute('aria-hidden', 'true');
2028
+ if (doc.createElementNS) {
2029
+ refreshMark.setAttribute('viewBox', '0 0 24 24'); refreshMark.setAttribute('fill', 'none');
2030
+ refreshMark.setAttribute('stroke', 'currentColor'); refreshMark.setAttribute('stroke-width', '1.7');
2031
+ refreshMark.setAttribute('stroke-linecap', 'round'); refreshMark.setAttribute('stroke-linejoin', 'round');
2032
+ const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
2033
+ path.setAttribute('d', 'M20 7v5h-5M4 17v-5h5M6.1 6.1a8 8 0 0 1 13.2 3.2M4.7 14.7a8 8 0 0 0 13.2 3.2');
2034
+ refreshMark.append(path);
2035
+ }
2036
+ reloadButton.append(refreshMark);
2037
+ toolbarStatus.append(reloadButton);
1991
2038
  controls.append(toolbarStatus);
1992
2039
  wrapper.append(controls);
1993
2040
  wrapper.append(summaryLine(doc, aggregate, { loading }));
1994
- const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
2041
+ const emptyText = loading ? '' : state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
1995
2042
  ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
1996
2043
  : 'この期間の判断履歴はありません。';
1997
2044
  const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
@@ -2023,6 +2070,12 @@ export function createJudgmentHistoryUI({
2023
2070
  replaceNode(nextSearchInput, previousSearchInput);
2024
2071
  }
2025
2072
  root.replaceChildren(wrapper);
2073
+ if (filterExpanded && filterFocus) {
2074
+ const field = findFirst(filterPopover.panel, node => filterFocus.label
2075
+ ? attributeValue(node, 'aria-label') === filterFocus.label
2076
+ : filterFocus.period && attributeValue(node, 'data-period') === filterFocus.period);
2077
+ field?.focus?.();
2078
+ }
2026
2079
  if (state.focusSearch || previousSearchFocused) {
2027
2080
  const input = findFirst(root, (node) => node.tagName === 'INPUT');
2028
2081
  input?.focus?.();
@@ -2048,8 +2101,38 @@ export function createJudgmentHistoryUI({
2048
2101
  onCopyReference: (payload) => void copyReference(payload),
2049
2102
  }
2050
2103
  : {};
2051
- renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, { ...railOptions, loading });
2052
- if (state.lastFocusedKey) {
2104
+ if (nativeDrawer) {
2105
+ if (selected) {
2106
+ if (!drawer || drawerKey !== selected.key) {
2107
+ if (drawer) disposeDrawer();
2108
+ drawerKey = selected.key;
2109
+ drawerContent = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-detail' });
2110
+ const selectedKey = selected.key;
2111
+ drawer = workspaceDialog(doc, {
2112
+ title: '判断の詳細', content: drawerContent, presentation: 'drawer',
2113
+ opener: () => findFirst(root, node => attributeValue(node, 'data-key') === selectedKey),
2114
+ onClose: () => {
2115
+ if (suppressDrawerClose) return;
2116
+ drawer = null; drawerContent = null; drawerKey = null;
2117
+ if (state.disposed) return;
2118
+ state.selectedKey = null; state.detailPhase = 'idle'; state.detailError = null;
2119
+ state.lastFocusedKey = selectedKey; render();
2120
+ },
2121
+ });
2122
+ }
2123
+ const detailFocus = findFirst(drawerContent, node => node === doc.activeElement);
2124
+ const detailLabel = attributeValue(detailFocus, 'aria-label');
2125
+ const detailSelection = Number.isInteger(detailFocus?.selectionStart)
2126
+ ? [detailFocus.selectionStart, detailFocus.selectionEnd] : null;
2127
+ renderRail(doc, drawerContent, selected, selectedDetail, null, { ...railOptions, loading });
2128
+ if (detailLabel) {
2129
+ const field = findFirst(drawerContent, node => attributeValue(node, 'aria-label') === detailLabel);
2130
+ field?.focus?.();
2131
+ if (detailSelection) field?.setSelectionRange?.(...detailSelection);
2132
+ }
2133
+ } else if (drawer) drawer.close();
2134
+ } else renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, { ...railOptions, loading });
2135
+ if (state.lastFocusedKey && !drawer) {
2053
2136
  const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
2054
2137
  row?.focus?.();
2055
2138
  state.lastFocusedKey = null;
@@ -2169,10 +2252,15 @@ export function createJudgmentHistoryUI({
2169
2252
  state.disposed = true;
2170
2253
  state.generation += 1;
2171
2254
  clearLoadTimers();
2255
+ filterPopover?.destroy();
2256
+ dismissLoading?.();
2257
+ notifications.remove?.();
2258
+ disposeDrawer();
2172
2259
  root.removeEventListener?.('keydown', handleKeyDown);
2173
2260
  rail?.removeEventListener?.('keydown', handleKeyDown);
2174
2261
  root.replaceChildren();
2175
2262
  rail?.replaceChildren();
2263
+ if (nativeDrawer && rail) rail.hidden = previousRailHidden;
2176
2264
  }
2177
2265
 
2178
2266
  const controller = { load, render, dispose, openDecision, get state() { return { ...state }; } };
@@ -451,8 +451,11 @@ export function createLocalWebShell({
451
451
  workspace.append(mobileBar, main);
452
452
  const railAside = makeElement(doc, 'aside', { className: 'bb-shell-rail', attrs: { 'aria-label': '選択中の項目' } });
453
453
  const rails = new Map();
454
+ const nativeDrawers = Boolean(doc.body && typeof doc.createElement('dialog').showModal === 'function');
454
455
  for (const screen of screens) {
455
- if (!screen.rail) continue;
456
+ // The built-in history uses a modal drawer in browsers, with a rail fallback
457
+ // for minimal hosts. Do not reserve an empty desktop column for the drawer.
458
+ if (!screen.rail || (nativeDrawers && screen.mount === mountToday)) continue;
456
459
  const railSlot = makeElement(doc, 'div', { className: 'bb-shell-rail-slot', attrs: { 'data-rail': screen.id } });
457
460
  railSlot.hidden = true;
458
461
  rails.set(screen.id, railSlot);