@unson/brainbase-mcp 0.41.4 → 0.41.6
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 +2 -2
- package/ui/brainbase-tokens.css +4 -0
- package/ui/foundation-overview.js +30 -11
- package/ui/judgment-history.css +34 -0
- package/ui/judgment-history.js +154 -66
- package/ui/local-web-shell.js +4 -1
- package/ui/people-and-companies-view.css +143 -0
- package/ui/people-and-companies-view.js +87 -11
- package/ui/workspace-kit.css +43 -0
- package/ui/workspace-kit.js +131 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.6",
|
|
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": "
|
|
486
|
+
"gitHead": "0c67baf7d7c57f4f671c966c8053705af65ba4d0"
|
|
487
487
|
}
|
package/ui/brainbase-tokens.css
CHANGED
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
--bb-color-notice-control-border: #ffffff45;
|
|
63
63
|
--bb-color-inline-error: #9b3d3d;
|
|
64
64
|
--bb-color-record-missing: #80601c;
|
|
65
|
+
--bb-color-drawer-overlay: rgb(13 25 46 / 6%);
|
|
66
|
+
--bb-color-drawer-overlay-clear: rgb(13 25 46 / 0%);
|
|
67
|
+
--bb-shadow-drawer: -18px 0 48px rgb(13 25 46 / 10%);
|
|
68
|
+
--bb-shadow-popover: 0 8px 28px rgb(13 25 46 / 14%);
|
|
65
69
|
--bb-color-dialog-overlay: #0d192e66;
|
|
66
70
|
--bb-color-scrollbar-rgb: 36,50,75;
|
|
67
71
|
--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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/ui/judgment-history.css
CHANGED
|
@@ -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; }
|
package/ui/judgment-history.js
CHANGED
|
@@ -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 =
|
|
1282
|
-
|
|
1283
|
-
|
|
1286
|
+
const banner = workspaceErrorCard(doc, {
|
|
1287
|
+
title: label,
|
|
1288
|
+
tone,
|
|
1289
|
+
message: note || '取得できた記録だけを表示しています。全体の取得状態は通知の詳細で確認できます。',
|
|
1284
1290
|
});
|
|
1285
|
-
banner.
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
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', {
|
|
2015
|
+
if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
|
|
2016
|
+
className: 'bb-jh-checked', text: stamp, attrs: { 'aria-label': `${stamp}に確認` },
|
|
2017
|
+
}));
|
|
1983
2018
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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
|
-
|
|
2052
|
-
|
|
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 }; } };
|
package/ui/local-web-shell.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -119,3 +119,146 @@
|
|
|
119
119
|
display: none;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/* Marks, counts and badges (2026-10). A person is a round mark, a company a
|
|
124
|
+
rounded square; both keep one tone per id. Counts sit next to each group
|
|
125
|
+
title, and the jump links at the top move to the people or company part. */
|
|
126
|
+
.bb-pc-jump {
|
|
127
|
+
display: flex;
|
|
128
|
+
gap: 4px;
|
|
129
|
+
margin: 4px 0 18px;
|
|
130
|
+
padding: 3px;
|
|
131
|
+
width: max-content;
|
|
132
|
+
border-radius: 9px;
|
|
133
|
+
background: var(--bb-color-canvas);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.bb-pc-jump-link {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 6px;
|
|
140
|
+
min-height: 28px;
|
|
141
|
+
padding: 0 12px;
|
|
142
|
+
border: 0;
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
background: transparent;
|
|
145
|
+
color: var(--bb-color-ink);
|
|
146
|
+
font: inherit;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.bb-pc-jump-link:hover {
|
|
153
|
+
background: var(--bb-color-paper);
|
|
154
|
+
box-shadow: 0 1px 2px rgb(32 33 36 / 10%);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.bb-pc-count {
|
|
158
|
+
color: var(--bb-color-muted);
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
font-variant-numeric: tabular-nums;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.bb-graph .bb-pc-group-title,
|
|
165
|
+
.bb-graph .bb-pc-company-title {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 8px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.bb-graph .bb-pc-company-title {
|
|
172
|
+
color: var(--bb-color-ink);
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.bb-pc-mark {
|
|
177
|
+
display: inline-grid;
|
|
178
|
+
flex: 0 0 auto;
|
|
179
|
+
place-items: center;
|
|
180
|
+
width: 24px;
|
|
181
|
+
height: 24px;
|
|
182
|
+
margin-right: 10px;
|
|
183
|
+
color: var(--bb-pc-mark-ink);
|
|
184
|
+
background: var(--bb-pc-mark-bg);
|
|
185
|
+
font-size: 11px;
|
|
186
|
+
font-weight: 700;
|
|
187
|
+
line-height: 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.bb-pc-person-mark { border-radius: 50%; }
|
|
191
|
+
.bb-pc-company-mark { border-radius: 6px; }
|
|
192
|
+
.bb-pc-company-title .bb-pc-mark,
|
|
193
|
+
.bb-pc-company-chip .bb-pc-mark { width: 18px; height: 18px; margin-right: 0; border-radius: 5px; font-size: 10px; }
|
|
194
|
+
|
|
195
|
+
.bb-pc-tone-0 { --bb-pc-mark-bg: #fde7e9; --bb-pc-mark-ink: #b4233a; }
|
|
196
|
+
.bb-pc-tone-1 { --bb-pc-mark-bg: #e3effd; --bb-pc-mark-ink: #1f5fbf; }
|
|
197
|
+
.bb-pc-tone-2 { --bb-pc-mark-bg: #e2f5ea; --bb-pc-mark-ink: #157046; }
|
|
198
|
+
.bb-pc-tone-3 { --bb-pc-mark-bg: #fff1d6; --bb-pc-mark-ink: #9a5a00; }
|
|
199
|
+
.bb-pc-tone-4 { --bb-pc-mark-bg: #efe7fd; --bb-pc-mark-ink: #6d36c9; }
|
|
200
|
+
.bb-pc-tone-5 { --bb-pc-mark-bg: #e0f4f6; --bb-pc-mark-ink: #0d6e7a; }
|
|
201
|
+
|
|
202
|
+
.bb-pc-people-ledger .bb-ws-ledger-object,
|
|
203
|
+
.bb-pc-company-ledger .bb-ws-ledger-object {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
min-width: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.bb-pc-people-ledger .bb-ws-ledger-object strong,
|
|
210
|
+
.bb-pc-company-ledger .bb-ws-ledger-object strong {
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
font-weight: 600;
|
|
213
|
+
text-overflow: ellipsis;
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.bb-pc-company-chips {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
gap: 4px 12px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.bb-pc-company-chip {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 7px;
|
|
227
|
+
min-width: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.bb-pc-kinds {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-wrap: wrap;
|
|
233
|
+
gap: 4px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.bb-pc-kind {
|
|
237
|
+
display: inline-flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 5px;
|
|
240
|
+
padding: 1px 8px;
|
|
241
|
+
border-radius: 5px;
|
|
242
|
+
background: var(--bb-color-canvas);
|
|
243
|
+
color: var(--bb-color-muted);
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.bb-pc-kind::before {
|
|
249
|
+
content: '';
|
|
250
|
+
width: 6px;
|
|
251
|
+
height: 6px;
|
|
252
|
+
border-radius: 50%;
|
|
253
|
+
background: currentColor;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.bb-pc-kind-customer { background: #e2f5ea; color: #157046; }
|
|
257
|
+
.bb-pc-kind-partner { background: #e3effd; color: #1f5fbf; }
|
|
258
|
+
.bb-pc-kind-prospect { background: #fff1d6; color: #9a5a00; }
|
|
259
|
+
|
|
260
|
+
.bb-pc-people-ledger .bb-ws-ledger-row:not(.bb-ws-ledger-head),
|
|
261
|
+
.bb-pc-company-ledger .bb-ws-ledger-row:not(.bb-ws-ledger-head) {
|
|
262
|
+
align-items: center;
|
|
263
|
+
min-height: 46px;
|
|
264
|
+
}
|
|
@@ -117,6 +117,21 @@ function ownKey(table, key) {
|
|
|
117
117
|
return typeof key === 'string' && Object.hasOwn(table, key) ? table[key] : undefined;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** First visible character of a name for its mark, or a placeholder. */
|
|
121
|
+
function nameInitial(name) {
|
|
122
|
+
const text = String(name ?? '').trim();
|
|
123
|
+
const latin = text.split(/\s+/u).filter(Boolean);
|
|
124
|
+
if (/^[A-Za-z]/u.test(text) && latin.length > 1) return `${latin[0][0]}${latin[1][0]}`.toUpperCase();
|
|
125
|
+
return ([...text][0] ?? '?').toUpperCase();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A stable tone (0-5) per id so a person or company keeps its color. */
|
|
129
|
+
function markTone(id) {
|
|
130
|
+
let hash = 0;
|
|
131
|
+
for (const character of String(id ?? '')) hash = (hash * 31 + character.codePointAt(0)) >>> 0;
|
|
132
|
+
return hash % 6;
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
/** The read is `ok` only when every person and company is well formed. */
|
|
121
136
|
export function normalizePeopleAndCompanies(result) {
|
|
122
137
|
if (result?.state !== 'ok') return result ?? INVALID;
|
|
@@ -269,8 +284,8 @@ export function createPeopleAndCompaniesView({
|
|
|
269
284
|
selected: person.id === state.detail?.id,
|
|
270
285
|
onSelect: (id) => void controller.openEntity(id),
|
|
271
286
|
cells: [
|
|
272
|
-
{ primary: person.isOwner ? `${person.name}(あなた)` : person.name,
|
|
273
|
-
person.companyIds.length > 0 ? person.companyIds
|
|
287
|
+
{ primary: person.isOwner ? `${person.name}(あなた)` : person.name, leading: personMark(person) },
|
|
288
|
+
person.companyIds.length > 0 ? companyChips(person.companyIds) : companyCell(person, ownId),
|
|
274
289
|
person.accountableProjectCount > 0
|
|
275
290
|
? `${person.projectCount}件(うち責任 ${person.accountableProjectCount}件)`
|
|
276
291
|
: `${person.projectCount}件`,
|
|
@@ -280,6 +295,67 @@ export function createPeopleAndCompaniesView({
|
|
|
280
295
|
}));
|
|
281
296
|
}
|
|
282
297
|
|
|
298
|
+
function personMark(person) {
|
|
299
|
+
return makeElement(doc, 'span', {
|
|
300
|
+
className: `bb-pc-mark bb-pc-person-mark bb-pc-tone-${markTone(person.id)}`,
|
|
301
|
+
text: nameInitial(person.name),
|
|
302
|
+
attrs: { 'aria-hidden': 'true' },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function companyMark(id, name) {
|
|
307
|
+
return makeElement(doc, 'span', {
|
|
308
|
+
className: `bb-pc-mark bb-pc-company-mark bb-pc-tone-${markTone(id)}`,
|
|
309
|
+
text: nameInitial(name),
|
|
310
|
+
attrs: { 'aria-hidden': 'true' },
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function companyChips(ids) {
|
|
315
|
+
const list = makeElement(doc, 'span', { className: 'bb-pc-company-chips' });
|
|
316
|
+
for (const id of ids) {
|
|
317
|
+
const chip = makeElement(doc, 'span', { className: 'bb-pc-company-chip' });
|
|
318
|
+
chip.append(companyMark(id, nameOf(id)), makeElement(doc, 'span', { text: nameOf(id) }));
|
|
319
|
+
list.append(chip);
|
|
320
|
+
}
|
|
321
|
+
return list;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function kindBadges(company) {
|
|
325
|
+
const kinds = company.kinds ?? [];
|
|
326
|
+
if (kinds.length === 0) return '';
|
|
327
|
+
const list = makeElement(doc, 'span', { className: 'bb-pc-kinds' });
|
|
328
|
+
for (const item of kinds) {
|
|
329
|
+
list.append(makeElement(doc, 'span', {
|
|
330
|
+
className: `bb-pc-kind bb-pc-kind-${Object.hasOwn(COMPANY_KIND_LABELS, item.kind) ? item.kind : 'other'}`,
|
|
331
|
+
text: ownKey(COMPANY_KIND_LABELS, item.kind) ?? item.kind,
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
return list;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function groupTitle(tag, text, count, { className = 'bb-pc-group-title', leading } = {}) {
|
|
338
|
+
const title = makeElement(doc, tag, { className });
|
|
339
|
+
if (leading) title.append(leading);
|
|
340
|
+
title.append(makeElement(doc, 'span', { text }), makeElement(doc, 'span', { className: 'bb-pc-count', text: String(count) }));
|
|
341
|
+
return title;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function jumpLinks(payload) {
|
|
345
|
+
const people = payload.people.filter((person) => person.active).length;
|
|
346
|
+
const companies = payload.companies.filter((company) => company.active).length;
|
|
347
|
+
const nav = makeElement(doc, 'nav', { className: 'bb-pc-jump', attrs: { 'aria-label': '人と会社の段' } });
|
|
348
|
+
for (const [label, count, target] of [['人', people, 'bb-pc-people'], ['会社', companies, 'bb-pc-companies']]) {
|
|
349
|
+
const link = button(doc, '', () => {
|
|
350
|
+
const node = typeof doc.getElementById === 'function' ? doc.getElementById(target) : null;
|
|
351
|
+
node?.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
|
|
352
|
+
}, { className: 'bb-pc-jump-link', attrs: { 'aria-label': `${label} ${count}件の段へ移る` } });
|
|
353
|
+
link.append(makeElement(doc, 'span', { text: label }), makeElement(doc, 'span', { className: 'bb-pc-count', text: String(count) }));
|
|
354
|
+
nav.append(link);
|
|
355
|
+
}
|
|
356
|
+
return nav;
|
|
357
|
+
}
|
|
358
|
+
|
|
283
359
|
function orgNoteText(note) {
|
|
284
360
|
return `記録上のメモ: ${note}(所属ではありません)`;
|
|
285
361
|
}
|
|
@@ -324,7 +400,7 @@ export function createPeopleAndCompaniesView({
|
|
|
324
400
|
}
|
|
325
401
|
|
|
326
402
|
function peopleSection(payload) {
|
|
327
|
-
const section = makeElement(doc, 'section', { className: 'bb-pc-people', attrs: { 'aria-label': '人' } });
|
|
403
|
+
const section = makeElement(doc, 'section', { className: 'bb-pc-people', attrs: { 'aria-label': '人', id: 'bb-pc-people' } });
|
|
328
404
|
section.append(workspaceSectionTitle(doc, { title: '人', lead: '社内か社外かは、所属(会社への所属の関係)と自社から決めています。印は別に持っていません。' }));
|
|
329
405
|
if (can.createPerson) section.append(sectionActions('人を加える', () => openCreateEntity('person')));
|
|
330
406
|
const groups = groupPeople(payload);
|
|
@@ -337,22 +413,22 @@ export function createPeopleAndCompaniesView({
|
|
|
337
413
|
}));
|
|
338
414
|
}
|
|
339
415
|
if (groups.ownCompanyId) {
|
|
340
|
-
section.append(
|
|
416
|
+
section.append(groupTitle('h3', `社内(${nameOf(groups.ownCompanyId)})`, groups.internal.length));
|
|
341
417
|
section.append(peopleLedger('社内の人', groups.internal, '自社に所属する人の記録はありません。'));
|
|
342
|
-
section.append(
|
|
418
|
+
section.append(groupTitle('h3', '社外', groups.external.reduce((sum, group) => sum + group.people.length, 0)));
|
|
343
419
|
if (groups.external.length === 0) section.append(makeElement(doc, 'p', { className: 'bb-pc-empty', text: 'ほかの会社に所属する人の記録はありません。' }));
|
|
344
420
|
for (const group of groups.external) {
|
|
345
|
-
section.append(
|
|
421
|
+
section.append(groupTitle('h4', group.company.name, group.people.length, { className: 'bb-pc-company-title', leading: companyMark(group.company.id, group.company.name) }));
|
|
346
422
|
section.append(peopleLedger(`${group.company.name}の人`, group.people));
|
|
347
423
|
}
|
|
348
424
|
}
|
|
349
|
-
section.append(
|
|
425
|
+
section.append(groupTitle('h3', '所属未確認', groups.unknown.length));
|
|
350
426
|
section.append(peopleLedger('所属未確認の人', groups.unknown, '所属が分からない人はいません。'));
|
|
351
427
|
return section;
|
|
352
428
|
}
|
|
353
429
|
|
|
354
430
|
function companySection(payload) {
|
|
355
|
-
const section = makeElement(doc, 'section', { className: 'bb-pc-companies', attrs: { 'aria-label': '会社' } });
|
|
431
|
+
const section = makeElement(doc, 'section', { className: 'bb-pc-companies', attrs: { 'aria-label': '会社', id: 'bb-pc-companies' } });
|
|
356
432
|
section.append(workspaceSectionTitle(doc, {
|
|
357
433
|
title: '会社',
|
|
358
434
|
lead: '自社を先頭に、顧客・協業先など関わる会社を並べています。種類は、会社の属性、プロジェクトとの関係の役割、提案中の案件の記録から出しています。記録が無い会社には種類を出しません。',
|
|
@@ -369,8 +445,8 @@ export function createPeopleAndCompaniesView({
|
|
|
369
445
|
selected: company.id === state.detail?.id,
|
|
370
446
|
onSelect: (id) => void controller.openEntity(id),
|
|
371
447
|
cells: [
|
|
372
|
-
{ primary: company.id === payload.ownCompany.id ? `${company.name}(自社)` : company.name,
|
|
373
|
-
|
|
448
|
+
{ primary: company.id === payload.ownCompany.id ? `${company.name}(自社)` : company.name, leading: companyMark(company.id, company.name) },
|
|
449
|
+
kindBadges(company),
|
|
374
450
|
`${company.memberIds.length}人`,
|
|
375
451
|
company.relatedProjects.length > 0
|
|
376
452
|
? `${company.projectIds.length}件(関わる ${company.relatedProjects.length}件)`
|
|
@@ -408,7 +484,7 @@ export function createPeopleAndCompaniesView({
|
|
|
408
484
|
return [...children, graphHostEmptyNotice(doc, emptyNotice)
|
|
409
485
|
?? workspaceNotice(doc, { label: '未登録', tone: 'info', role: 'status', text: 'Graphに人と会社の記録はまだありません。' })];
|
|
410
486
|
}
|
|
411
|
-
return [...children, peopleSection(payload), companySection(payload), endedNote(payload)];
|
|
487
|
+
return [...children, jumpLinks(payload), peopleSection(payload), companySection(payload), endedNote(payload)];
|
|
412
488
|
}
|
|
413
489
|
|
|
414
490
|
// -------------------------------------------------------------------------
|
package/ui/workspace-kit.css
CHANGED
|
@@ -530,3 +530,46 @@
|
|
|
530
530
|
.bb-ws-error-card :is(summary,button):focus-visible { outline: 2px solid var(--bb-color-notice-text); outline-offset: 2px; }
|
|
531
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
532
|
@media (prefers-reduced-motion: reduce) { .bb-ws-help-bubble { transition: none; } }
|
|
533
|
+
|
|
534
|
+
/* One native-dialog drawer presentation for shared parts and host-owned task dialogs. */
|
|
535
|
+
.bb-ws-drawer {
|
|
536
|
+
box-sizing: border-box; width: min(640px,100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;
|
|
537
|
+
margin: 0 0 0 auto; padding: 0; border: 0; border-left: 1px solid var(--bb-color-border);
|
|
538
|
+
border-radius: 0; background: var(--bb-color-panel); color: var(--bb-color-ink); box-shadow: var(--bb-shadow-drawer); overflow: auto;
|
|
539
|
+
}
|
|
540
|
+
.bb-ws-drawer[open] { animation: bb-ws-drawer-in 180ms ease-out; }
|
|
541
|
+
.bb-ws-drawer::backdrop { background: var(--bb-color-drawer-overlay); }
|
|
542
|
+
.bb-ws-drawer.is-closing { animation: bb-ws-drawer-out 280ms ease-in both; pointer-events: none; }
|
|
543
|
+
.bb-ws-drawer.is-closing::backdrop { animation: bb-ws-drawer-backdrop-out 280ms ease-in both; }
|
|
544
|
+
.bb-ws-drawer .bb-ws-dialog-head { align-items: center; padding: 14px 16px 14px 28px; }
|
|
545
|
+
.bb-ws-drawer .bb-ws-dialog-head h2 { font-weight: 650; line-height: 1.35; }
|
|
546
|
+
.bb-ws-drawer .bb-ws-dialog-body { padding: 18px 28px 32px; }
|
|
547
|
+
@keyframes bb-ws-drawer-in { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
548
|
+
@keyframes bb-ws-drawer-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(32px); opacity: 0; } }
|
|
549
|
+
@keyframes bb-ws-drawer-backdrop-out { from { background: var(--bb-color-drawer-overlay); } to { background: var(--bb-color-drawer-overlay-clear); } }
|
|
550
|
+
.bb-ws-filter-anchor { position: relative; display: inline-flex; flex: none; }
|
|
551
|
+
.bb-ws-filter-anchor > .bb-ws-button::after { content: '⌄'; margin-left: var(--bb-space-2); }
|
|
552
|
+
.bb-ws-filter-popover {
|
|
553
|
+
position: absolute; z-index: 40; top: calc(100% + 8px); right: 0; display: grid; gap: var(--bb-space-4);
|
|
554
|
+
box-sizing: border-box; width: 360px; max-width: calc(100vw - 48px); padding: 20px;
|
|
555
|
+
border: 1px solid var(--bb-color-border); border-radius: 12px; background: var(--bb-color-panel); color: var(--bb-color-ink);
|
|
556
|
+
box-shadow: var(--bb-shadow-popover); max-height: calc(100dvh - 220px); overflow-y: auto;
|
|
557
|
+
transform-origin: top right; animation: bb-ws-filter-enter 180ms ease-out;
|
|
558
|
+
}
|
|
559
|
+
.bb-ws-filter-popover[hidden] { display: none !important; }
|
|
560
|
+
@keyframes bb-ws-filter-enter { from { opacity: 0; transform: translateY(-6px) scaleY(.96); } to { opacity: 1; transform: none; } }
|
|
561
|
+
.bb-ws-loading-card { align-items: center; border-left-color: var(--bb-color-notice-control-border); }
|
|
562
|
+
.bb-ws-loading-spinner { position: relative; display: block; flex: none; width: 14px; height: 14px; }
|
|
563
|
+
.bb-ws-loading-spinner::before {
|
|
564
|
+
content: ''; position: absolute; top: 1px; left: 1px; box-sizing: border-box; width: 12px; height: 12px;
|
|
565
|
+
border: 1.5px solid var(--bb-color-notice-control-border); border-top-color: var(--bb-color-notice-text); border-radius: 50%;
|
|
566
|
+
transform-origin: center; animation: bb-ws-spinner 1s linear infinite;
|
|
567
|
+
}
|
|
568
|
+
@keyframes bb-ws-spinner { to { transform: rotate(360deg); } }
|
|
569
|
+
.bb-ws-error-card.is-dismissing { animation: bb-ws-notification-out 450ms ease both; pointer-events: none; }
|
|
570
|
+
@keyframes bb-ws-notification-out { to { opacity: 0; transform: translateX(28px); } }
|
|
571
|
+
@media (max-width:760px) { .bb-ws-drawer { width: 100vw; border-left: 0; } }
|
|
572
|
+
@media (prefers-reduced-motion:reduce) {
|
|
573
|
+
.bb-ws-drawer[open], .bb-ws-drawer.is-closing, .bb-ws-drawer.is-closing::backdrop,
|
|
574
|
+
.bb-ws-filter-popover, .bb-ws-loading-spinner::before, .bb-ws-error-card.is-dismissing { animation: none; }
|
|
575
|
+
}
|
package/ui/workspace-kit.js
CHANGED
|
@@ -256,23 +256,150 @@ export function workspaceHelp(doc, { text, label } = {}) {
|
|
|
256
256
|
wrap.append(control, bubble); return wrap;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
const drawerClosings = new WeakMap();
|
|
260
|
+
const notificationClosings = new WeakMap();
|
|
261
|
+
const reducedMotion = node => node.ownerDocument?.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
262
|
+
|
|
263
|
+
/** Apply the same native-dialog presentation to host-owned and kit drawers. */
|
|
264
|
+
export function workspaceApplyDrawer(dialog) {
|
|
265
|
+
dialog.classList.add('bb-ws-drawer');
|
|
266
|
+
return dialog;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Keep the modal/focus trap alive until the exit motion finishes. Returns cancellation cleanup. */
|
|
270
|
+
export function workspaceCloseDrawer(dialog, finish) {
|
|
271
|
+
if (drawerClosings.has(dialog)) return drawerClosings.get(dialog);
|
|
272
|
+
if (!dialog.open || reducedMotion(dialog)) { finish(); return () => {}; }
|
|
273
|
+
let timer;
|
|
274
|
+
let active = true;
|
|
275
|
+
const cancel = () => {
|
|
276
|
+
if (!active) return;
|
|
277
|
+
active = false;
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
dialog.removeEventListener('animationend', ended);
|
|
280
|
+
dialog.removeEventListener('close', cancel);
|
|
281
|
+
dialog.classList.remove('is-closing');
|
|
282
|
+
drawerClosings.delete(dialog);
|
|
283
|
+
};
|
|
284
|
+
const complete = () => { if (!active) return; cancel(); finish(); };
|
|
285
|
+
const ended = event => {
|
|
286
|
+
if (event.target === dialog && event.animationName === 'bb-ws-drawer-out') complete();
|
|
287
|
+
};
|
|
288
|
+
drawerClosings.set(dialog, cancel);
|
|
289
|
+
dialog.addEventListener('animationend', ended);
|
|
290
|
+
dialog.addEventListener('close', cancel);
|
|
291
|
+
dialog.classList.add('is-closing');
|
|
292
|
+
timer = setTimeout(complete, 360); // Animation cancellation / background-tab fallback.
|
|
293
|
+
return cancel;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Native modal, optionally a right drawer. close() animates; dispose() tears down immediately. */
|
|
297
|
+
export function workspaceDialog(doc, { title, content, opener, presentation = 'center', onClose } = {}) {
|
|
261
298
|
const dialog = el(doc, 'dialog', { className: 'bb-ws-dialog' });
|
|
299
|
+
if (presentation === 'drawer') workspaceApplyDrawer(dialog);
|
|
262
300
|
const headingId = `bb-ws-dialog-${++workspaceSequence}`;
|
|
263
301
|
dialog.setAttribute('aria-labelledby', headingId);
|
|
264
302
|
const head = el(doc, 'header', { className: 'bb-ws-dialog-head' });
|
|
303
|
+
const nativeClose = dialog.close.bind(dialog);
|
|
304
|
+
let disposed = false;
|
|
305
|
+
const cleanup = () => {
|
|
306
|
+
if (disposed) return;
|
|
307
|
+
disposed = true;
|
|
308
|
+
drawerClosings.get(dialog)?.();
|
|
309
|
+
dialog.remove();
|
|
310
|
+
try { onClose?.(); } finally {
|
|
311
|
+
const target = typeof opener === 'function' ? opener() : opener;
|
|
312
|
+
if (target?.isConnected) target.focus();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
dialog.close = value => {
|
|
316
|
+
if (disposed) return;
|
|
317
|
+
const finish = () => { nativeClose(value); cleanup(); };
|
|
318
|
+
if (presentation === 'drawer') workspaceCloseDrawer(dialog, finish);
|
|
319
|
+
else finish();
|
|
320
|
+
};
|
|
321
|
+
dialog.dispose = () => {
|
|
322
|
+
if (disposed) return;
|
|
323
|
+
drawerClosings.get(dialog)?.();
|
|
324
|
+
nativeClose(); cleanup();
|
|
325
|
+
};
|
|
265
326
|
const close = workspaceButton(doc, { text: '×', attrs: { 'aria-label': '閉じる' }, onClick: () => dialog.close() });
|
|
266
327
|
head.append(el(doc, 'h2', { text: title, attrs: { id: headingId } }), close);
|
|
267
328
|
const body = el(doc, 'div', { className: 'bb-ws-dialog-body' }); appendContent(body, doc, content);
|
|
268
329
|
dialog.append(head, body); doc.body.append(dialog);
|
|
330
|
+
dialog.addEventListener('cancel', event => { event.preventDefault(); dialog.close(); });
|
|
269
331
|
dialog.addEventListener('click', event => {
|
|
270
332
|
if (event.target !== dialog) return;
|
|
271
333
|
const rect = dialog.getBoundingClientRect();
|
|
272
334
|
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
|
|
273
335
|
});
|
|
274
|
-
dialog.addEventListener('close',
|
|
275
|
-
dialog.showModal(); close.focus();
|
|
336
|
+
dialog.addEventListener('close', cleanup, { once: true });
|
|
337
|
+
try { dialog.showModal(); close.focus(); } catch (error) { cleanup(); throw error; }
|
|
338
|
+
return dialog;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Anchored advanced search. Hosts destroy it before replacing its controls. */
|
|
342
|
+
export function workspaceFilterPopover(doc, { content, open = false, onToggle, trigger, panel: suppliedPanel, initialFocus } = {}) {
|
|
343
|
+
const root = el(doc, 'span', { className: 'bb-ws-filter-anchor' });
|
|
344
|
+
const panel = suppliedPanel || el(doc, 'div', { className: 'bb-ws-filter-popover', attrs: { id: `bb-ws-filter-${++workspaceSequence}`, role: 'region', 'aria-label': '詳細検索' } });
|
|
345
|
+
if (!panel.id && !panel.getAttribute?.('id')) panel.setAttribute('id', `bb-ws-filter-${++workspaceSequence}`);
|
|
346
|
+
panel.classList?.add('bb-ws-filter-popover');
|
|
347
|
+
const control = trigger || workspaceButton(doc, { text: '詳細検索', variant: 'quiet' });
|
|
348
|
+
control.setAttribute('aria-controls', panel.id || panel.getAttribute?.('id') || panel.attributes?.id);
|
|
349
|
+
appendContent(panel, doc, content); root.append(control, panel);
|
|
350
|
+
let destroyed = false;
|
|
351
|
+
const removeListeners = () => { doc.removeEventListener?.('pointerdown', outside); doc.removeEventListener?.('keydown', escape); };
|
|
352
|
+
const setOpen = (value, focus = false) => {
|
|
353
|
+
if (destroyed) return;
|
|
354
|
+
removeListeners(); panel.hidden = !value; control.setAttribute('aria-expanded', String(value));
|
|
355
|
+
onToggle?.(value);
|
|
356
|
+
if (value) {
|
|
357
|
+
doc.addEventListener?.('pointerdown', outside); doc.addEventListener?.('keydown', escape);
|
|
358
|
+
if (focus) (typeof initialFocus === 'function' ? initialFocus() : initialFocus || panel.querySelector?.('input:not(:disabled),select:not(:disabled),button:not(:disabled)'))?.focus();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const outside = event => { if (!root.contains?.(event.target)) setOpen(false); };
|
|
362
|
+
const escape = event => {
|
|
363
|
+
if (event.key !== 'Escape') return;
|
|
364
|
+
event.preventDefault(); event.stopPropagation(); setOpen(false); control.focus();
|
|
365
|
+
};
|
|
366
|
+
const toggle = () => setOpen(panel.hidden, true);
|
|
367
|
+
control.addEventListener('click', toggle); setOpen(open);
|
|
368
|
+
return { root, control, panel, destroy() {
|
|
369
|
+
destroyed = true; removeListeners(); control.removeEventListener?.('click', toggle);
|
|
370
|
+
panel.hidden = true; control.setAttribute('aria-expanded', 'false');
|
|
371
|
+
} };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Compact notification with a fixed-center, purely visual spinner. */
|
|
375
|
+
export function workspaceLoadingCard(doc, { title = 'ロード中' } = {}) {
|
|
376
|
+
const card = el(doc, 'section', { className: 'bb-ws-error-card bb-ws-loading-card', attrs: { role: 'status', 'aria-live': 'polite' } });
|
|
377
|
+
card.append(el(doc, 'span', { className: 'bb-ws-loading-spinner', attrs: { 'aria-hidden': 'true' } }), el(doc, 'span', { text: title }));
|
|
378
|
+
return card;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Fade right then remove. Calling the returned cancel function restores a reusable card. */
|
|
382
|
+
export function workspaceDismissNotification(card, onDismiss) {
|
|
383
|
+
const remove = () => {
|
|
384
|
+
if (typeof card.remove === 'function') card.remove();
|
|
385
|
+
else if (card.parentNode?.removeChild) card.parentNode.removeChild(card);
|
|
386
|
+
else if (card.parentNode?.replaceChildren) card.parentNode.replaceChildren(...Array.from(card.parentNode.children).filter(node => node !== card));
|
|
387
|
+
};
|
|
388
|
+
if (notificationClosings.has(card)) return notificationClosings.get(card);
|
|
389
|
+
if (!card.classList || reducedMotion(card)) { remove(); onDismiss?.(); return () => {}; }
|
|
390
|
+
let timer;
|
|
391
|
+
let active = true;
|
|
392
|
+
const cancel = () => {
|
|
393
|
+
if (!active) return;
|
|
394
|
+
active = false; clearTimeout(timer);
|
|
395
|
+
card.removeEventListener('animationend', ended);
|
|
396
|
+
card.classList.remove('is-dismissing'); notificationClosings.delete(card);
|
|
397
|
+
};
|
|
398
|
+
const complete = () => { if (!active) return; cancel(); remove(); onDismiss?.(); };
|
|
399
|
+
const ended = event => { if (event.target === card && event.animationName === 'bb-ws-notification-out') complete(); };
|
|
400
|
+
notificationClosings.set(card, cancel); card.addEventListener('animationend', ended);
|
|
401
|
+
card.classList.add('is-dismissing'); timer = setTimeout(complete, 500);
|
|
402
|
+
return cancel;
|
|
276
403
|
}
|
|
277
404
|
|
|
278
405
|
/** Persistent, initially closed notification. The caller owns its lifecycle. */
|