@unson/brainbase-mcp 0.41.1 → 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 +9 -0
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/canonical-task-service.js +10 -5
- package/dist/local-web-host.js +1 -1
- package/package.json +5 -3
- package/ui/brainbase-tokens.css +17 -0
- package/ui/foundation-overview.css +18 -0
- package/ui/foundation-overview.js +77 -83
- package/ui/graph-own-share.js +30 -3
- package/ui/judgment-history.js +0 -17
- package/ui/local-web-shell.css +1 -0
- package/ui/local-web-shell.js +3 -0
- package/ui/native-scrollbars.css +18 -0
- package/ui/native-scrollbars.js +18 -0
- package/ui/outcome-knowledge.css +5 -0
- package/ui/outcome-knowledge.js +1 -1
- package/ui/workspace-kit.css +36 -0
- package/ui/workspace-kit.js +65 -0
- package/ui/world/world-district.js +8 -0
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": "
|
|
28
|
+
"sha256": "55fc2b9c7945b81cfe3b7971bc1d067f8cd45d057fbf23dcf3dfae175838d1d3"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
-
"digest": "
|
|
31
|
+
"digest": "89b20a5aa122e73e9d2f7c99e1b029cb12b101b95574ee4560ce023ec3197304"
|
|
32
32
|
}
|
|
@@ -134,6 +134,11 @@ function normalizeLimit(value, max = MAX_LIMIT) {
|
|
|
134
134
|
}
|
|
135
135
|
return limit;
|
|
136
136
|
}
|
|
137
|
+
// Optional list filters are only sent to the repository when they carry a value.
|
|
138
|
+
// An empty value must mean "no filter", never "match IS NULL".
|
|
139
|
+
function optionalFilter(key, value) {
|
|
140
|
+
return value == null ? {} : { [key]: value };
|
|
141
|
+
}
|
|
137
142
|
function normalizeCursor(value) {
|
|
138
143
|
return normalizeString(value, 'cursor', { max: 500 });
|
|
139
144
|
}
|
|
@@ -885,7 +890,7 @@ export class CanonicalTaskService {
|
|
|
885
890
|
return patch;
|
|
886
891
|
}
|
|
887
892
|
normalizeListFilters(filters) {
|
|
888
|
-
const { status: _status, priority: _priority, projectCode: _projectCode, project_code: _project_code, ...canonicalFilters } = filters;
|
|
893
|
+
const { status: _status, priority: _priority, projectCode: _projectCode, project_code: _project_code, assigneePersonId: _assigneePersonId, dueAfter: _dueAfter, dueBefore: _dueBefore, cursor: _cursor, ...canonicalFilters } = filters;
|
|
889
894
|
const statuses = normalizeCollection(filters.statuses ?? filters.status, 'statuses');
|
|
890
895
|
const priorities = normalizeCollection(filters.priorities ?? filters.priority, 'priorities');
|
|
891
896
|
const projectCodes = normalizeProjectCodes(filters.projectCodes ?? filters.projectCode ?? filters.project_code);
|
|
@@ -894,11 +899,11 @@ export class CanonicalTaskService {
|
|
|
894
899
|
statuses,
|
|
895
900
|
priorities,
|
|
896
901
|
projectCodes,
|
|
897
|
-
assigneePersonId
|
|
898
|
-
dueAfter
|
|
899
|
-
dueBefore
|
|
902
|
+
...optionalFilter('assigneePersonId', normalizeString(filters.assigneePersonId, 'assigneePersonId', { max: 200 })),
|
|
903
|
+
...optionalFilter('dueAfter', normalizeIsoDate(filters.dueAfter, 'dueAfter')),
|
|
904
|
+
...optionalFilter('dueBefore', normalizeIsoDate(filters.dueBefore, 'dueBefore')),
|
|
900
905
|
limit: normalizeLimit(filters.limit),
|
|
901
|
-
cursor
|
|
906
|
+
...optionalFilter('cursor', normalizeCursor(filters.cursor)),
|
|
902
907
|
};
|
|
903
908
|
}
|
|
904
909
|
normalizeSearchFilters(filters) {
|
package/dist/local-web-host.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
486
|
+
"gitHead": "19cc7821541c7e563e4d08fd4b2b09936e80a701"
|
|
485
487
|
}
|
package/ui/brainbase-tokens.css
CHANGED
|
@@ -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; }
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
workspaceButton,
|
|
12
12
|
workspaceDefinition,
|
|
13
13
|
workspaceLedger,
|
|
14
|
-
workspaceMetrics,
|
|
15
14
|
workspaceNotice,
|
|
16
15
|
workspacePageHeader,
|
|
17
16
|
workspaceSectionTitle,
|
|
17
|
+
workspaceHelp, workspaceDialog, workspaceDisclosure, workspaceErrorCard,
|
|
18
18
|
} from './workspace-kit.js';
|
|
19
19
|
|
|
20
20
|
export const FOUNDATION_OVERVIEW_CONTRACT_VERSION = 'foundation-overview.v1';
|
|
@@ -430,59 +430,66 @@ function objectiveCard(doc, record, variableMap) {
|
|
|
430
430
|
return card;
|
|
431
431
|
}
|
|
432
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
|
+
|
|
433
462
|
function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
|
|
434
463
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '目的' } });
|
|
435
|
-
section.append(
|
|
436
|
-
title: '目的',
|
|
437
|
-
lead: '目指す状態、評価の定義、記録上の現状を分けて確認します。',
|
|
438
|
-
}));
|
|
464
|
+
section.append(topicTitle(doc, '目的', '目指す状態、評価の定義、記録上の現状を分けて確認します。'));
|
|
439
465
|
if (catalog.objectives.length === 0) {
|
|
440
466
|
section.append(workspaceNotice(doc, { label: '目的', text: 'この対象範囲には登録された目的がありません。' }));
|
|
441
467
|
return section;
|
|
442
468
|
}
|
|
443
|
-
const variableMap = new Map(catalog.variables.map(
|
|
444
|
-
const rows = catalog.objectives.map(
|
|
445
|
-
const definition = recordDefinition(record);
|
|
446
|
-
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);
|
|
447
472
|
const key = recordContexts?.get(record)?.key ?? `${definition.id}@${definition.revision}`;
|
|
448
473
|
return {
|
|
449
|
-
key,
|
|
450
|
-
selected: key === viewState.objectiveKey,
|
|
474
|
+
key, selected: key === viewState.objectiveKey,
|
|
451
475
|
onSelect: () => {
|
|
452
476
|
viewState.objectiveKey = key;
|
|
453
|
-
|
|
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
|
+
});
|
|
454
483
|
},
|
|
455
484
|
cells: [
|
|
456
|
-
{ primary: formatAny(
|
|
457
|
-
|
|
458
|
-
|
|
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),
|
|
459
489
|
],
|
|
460
490
|
};
|
|
461
491
|
});
|
|
462
|
-
section.append(workspaceLedger(doc, {
|
|
463
|
-
className: 'bb-fov-objective-ledger',
|
|
464
|
-
ariaLabel: '登録された目的',
|
|
465
|
-
columns: ['目的', '目指す状態', '登録状態'],
|
|
466
|
-
rows,
|
|
467
|
-
}));
|
|
468
|
-
const selected = catalog.objectives.find((record) => {
|
|
469
|
-
const definition = recordDefinition(record);
|
|
470
|
-
return (recordContexts?.get(record)?.key ?? `${definition.id}@${definition.revision}`) === viewState.objectiveKey;
|
|
471
|
-
}) ?? (recordContexts ? null : catalog.objectives[0]);
|
|
472
|
-
if (!selected && recordContexts) section.append(makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '目的の行を選ぶと、評価基準と記録上の現状を確認できます。' }));
|
|
473
|
-
if (selected) {
|
|
474
|
-
const selectedDefinition = recordDefinition(selected);
|
|
475
|
-
viewState.objectiveKey = recordContexts?.get(selected)?.key ?? `${selectedDefinition.id}@${selectedDefinition.revision}`;
|
|
476
|
-
if (catalog.objectives.length > 1) {
|
|
477
|
-
section.append(workspaceNotice(doc, {
|
|
478
|
-
label: '詳細表示',
|
|
479
|
-
text: `目的${catalog.objectives.length}件のうち1件を選択して詳細を表示しています。`,
|
|
480
|
-
}));
|
|
481
|
-
}
|
|
482
|
-
const card = objectiveCard(doc, selected, recordContexts?.get(selected)?.variableMap ?? variableMap);
|
|
483
|
-
appendRecordSource(doc, card, selected, recordContexts);
|
|
484
|
-
section.append(card);
|
|
485
|
-
}
|
|
492
|
+
section.append(workspaceLedger(doc, { className: 'bb-fov-objective-ledger', ariaLabel: '登録された目的', columns: ['目的', '現状', '確認待ち', '確認日'], rows }));
|
|
486
493
|
return section;
|
|
487
494
|
}
|
|
488
495
|
|
|
@@ -507,9 +514,7 @@ function philosophyCard(doc, record) {
|
|
|
507
514
|
const card = makeElement(doc, 'article', { className: 'bb-fov-philosophy-card' });
|
|
508
515
|
const title = formatAny(payload.title) ?? record.id;
|
|
509
516
|
card.append(makeElement(doc, 'h3', { text: title }));
|
|
510
|
-
|
|
511
|
-
const status = rawStatus ? (PHILOSOPHY_STATUS_LABELS[rawStatus] ?? `未確認(${rawStatus})`) : '状態未確認';
|
|
512
|
-
card.append(makeElement(doc, 'p', { className: 'bb-fov-status', text: `状態: ${status} / 版: ${record.revision}` }));
|
|
517
|
+
|
|
513
518
|
if (formatAny(payload.statement)) card.append(makeElement(doc, 'p', { className: 'bb-fov-philosophy-statement', text: formatAny(payload.statement) }));
|
|
514
519
|
if (formatAny(payload.summary)) card.append(makeElement(doc, 'p', { text: formatAny(payload.summary) }));
|
|
515
520
|
const applicability = record.applicability?.scope
|
|
@@ -524,10 +529,7 @@ function philosophyCard(doc, record) {
|
|
|
524
529
|
|
|
525
530
|
function renderPhilosophies(doc, catalog, recordContexts) {
|
|
526
531
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '哲学' } });
|
|
527
|
-
|
|
528
|
-
title: '哲学',
|
|
529
|
-
lead: '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。',
|
|
530
|
-
}));
|
|
532
|
+
|
|
531
533
|
if (catalog.philosophies.length === 0) {
|
|
532
534
|
section.append(workspaceNotice(doc, { label: '哲学', text: 'この対象範囲には登録された哲学がありません。' }));
|
|
533
535
|
return section;
|
|
@@ -535,8 +537,9 @@ function renderPhilosophies(doc, catalog, recordContexts) {
|
|
|
535
537
|
const grid = makeElement(doc, 'div', { className: 'bb-fov-philosophy-grid' });
|
|
536
538
|
for (const record of catalog.philosophies) {
|
|
537
539
|
const card = philosophyCard(doc, record);
|
|
538
|
-
|
|
539
|
-
|
|
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);
|
|
540
543
|
}
|
|
541
544
|
section.append(grid);
|
|
542
545
|
return section;
|
|
@@ -593,10 +596,7 @@ function modelCard(doc, record, variableMap) {
|
|
|
593
596
|
|
|
594
597
|
function renderModels(doc, catalog, viewState, callbacks, recordContexts) {
|
|
595
598
|
const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '世界モデル' } });
|
|
596
|
-
|
|
597
|
-
title: '世界モデル',
|
|
598
|
-
lead: '変数の関係として登録された見方です。仮説・未検証・許可用途を、それぞれの状態として表示します。',
|
|
599
|
-
}));
|
|
599
|
+
|
|
600
600
|
const searchWrap = makeElement(doc, 'div', { className: 'bb-fov-model-search' });
|
|
601
601
|
const label = makeElement(doc, 'label', { text: 'モデルを検索', attrs: { for: 'bb-fov-model-search' } });
|
|
602
602
|
const input = makeElement(doc, 'input', {
|
|
@@ -683,6 +683,7 @@ export function renderFoundationOverview(root, state, {
|
|
|
683
683
|
showHeader = true,
|
|
684
684
|
recordContexts,
|
|
685
685
|
partial = false,
|
|
686
|
+
onNotice,
|
|
686
687
|
} = {}) {
|
|
687
688
|
if (!root) throw new TypeError('root is required');
|
|
688
689
|
const doc = getDocument(explicitDocument);
|
|
@@ -699,33 +700,26 @@ export function renderFoundationOverview(root, state, {
|
|
|
699
700
|
source: `正本: ${scopeId}`,
|
|
700
701
|
}));
|
|
701
702
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
703
|
+
const notify = notice => {
|
|
704
|
+
if (onNotice) onNotice(notice);
|
|
705
|
+
else surface.append(workspaceErrorCard(doc, notice));
|
|
706
|
+
};
|
|
706
707
|
if (state.status === 'loading') {
|
|
707
|
-
surface.append(renderLoading(doc, state, callbacks));
|
|
708
|
-
root.append(surface);
|
|
709
|
-
return surface;
|
|
708
|
+
surface.append(renderLoading(doc, state, callbacks)); root.append(surface); return surface;
|
|
710
709
|
}
|
|
711
710
|
if (state.status !== 'ready') {
|
|
712
|
-
|
|
713
|
-
root.append(surface);
|
|
714
|
-
return surface;
|
|
711
|
+
notify({ title: '取得失敗', message: `${state.error?.message ?? '目的・哲学・世界モデルを確認できません。'} 登録なし・0件として表示していません。`, onRetry: callbacks.load });
|
|
712
|
+
root.append(surface); return surface;
|
|
715
713
|
}
|
|
716
714
|
const catalog = state.catalog;
|
|
717
|
-
if (partial)
|
|
718
|
-
surface.append(recordContexts
|
|
719
|
-
? makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '記録上の現状は保存時点の記述です。登録状態と検証状態を分けて読みます。' })
|
|
720
|
-
: workspaceNotice(doc, { label: '読み方', text: '登録は達成・採用・検証済みを意味しません。記録上の現状は保存時点の記述です。' }));
|
|
721
|
-
surface.append(workspaceMetrics(doc, [
|
|
722
|
-
{ label: '目的', value: catalog.objectives.length, note: partial ? '取得済みの定義(全体は未確認)' : '登録された定義' },
|
|
723
|
-
{ label: '哲学', value: catalog.philosophies.length, note: partial ? '取得済みの版(全体は未確認)' : '登録された版' },
|
|
724
|
-
{ label: '世界モデル', value: catalog.models.length, note: partial ? '取得済みの検索対象(全体は未確認)' : '検索対象' },
|
|
725
|
-
], { ariaLabel: 'Foundationの登録数' }));
|
|
715
|
+
if (partial) notify({ title: '取得済みの範囲', tone: 'warning', message: '以下の登録なし・検索結果は取得できた記録だけが対象です。全体は未確認です。' });
|
|
726
716
|
surface.append(renderObjectives(doc, catalog, viewState, callbacks, recordContexts));
|
|
727
|
-
|
|
728
|
-
|
|
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));
|
|
729
723
|
root.append(surface);
|
|
730
724
|
return surface;
|
|
731
725
|
}
|
|
@@ -737,7 +731,7 @@ function appendRecordSource(doc, card, record, contexts) {
|
|
|
737
731
|
|
|
738
732
|
/** Compose independently validated catalogs by topic, preserving each reference's source context. */
|
|
739
733
|
export function renderFoundationCollectionOverview(root, sources, {
|
|
740
|
-
document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体',
|
|
734
|
+
document: explicitDocument, viewState = { query: '', page: 0 }, callbacks = {}, scopeLabel = '全体', onNotice,
|
|
741
735
|
} = {}) {
|
|
742
736
|
const doc = getDocument(explicitDocument);
|
|
743
737
|
root.replaceChildren();
|
|
@@ -747,14 +741,12 @@ export function renderFoundationCollectionOverview(root, sources, {
|
|
|
747
741
|
let ready = 0;
|
|
748
742
|
for (const source of sources) {
|
|
749
743
|
if (source.state.status !== 'ready') {
|
|
750
|
-
|
|
751
|
-
label: source.label,
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
notice.append(workspaceButton(doc, { text: '再試行', variant: 'quiet', onClick: callbacks.load }));
|
|
757
|
-
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
|
+
}
|
|
758
750
|
continue;
|
|
759
751
|
}
|
|
760
752
|
const value = normalizeFoundationCatalog(source.state.catalog, source.scopeId);
|
|
@@ -786,7 +778,7 @@ export function renderFoundationCollectionOverview(root, sources, {
|
|
|
786
778
|
const mount = makeElement(doc, 'div');
|
|
787
779
|
renderFoundationOverview(mount, { status: 'ready', catalog }, {
|
|
788
780
|
document: doc, scopeId: scopeLabel, showHeader: false, viewState, callbacks,
|
|
789
|
-
recordContexts: contexts, partial: ready !== sources.length,
|
|
781
|
+
recordContexts: contexts, partial: ready !== sources.length, onNotice,
|
|
790
782
|
});
|
|
791
783
|
root.append(mount);
|
|
792
784
|
}
|
|
@@ -819,6 +811,7 @@ export function createFoundationOverview({ root, port, scopeId, document: explic
|
|
|
819
811
|
},
|
|
820
812
|
async load() {
|
|
821
813
|
if (destroyed) return state;
|
|
814
|
+
viewState.dialog?.close();
|
|
822
815
|
const requestGeneration = ++generation;
|
|
823
816
|
state.status = 'loading';
|
|
824
817
|
state.catalog = null;
|
|
@@ -847,6 +840,7 @@ export function createFoundationOverview({ root, port, scopeId, document: explic
|
|
|
847
840
|
destroy() {
|
|
848
841
|
destroyed = true;
|
|
849
842
|
generation += 1;
|
|
843
|
+
viewState.dialog?.close();
|
|
850
844
|
root.replaceChildren();
|
|
851
845
|
},
|
|
852
846
|
};
|
package/ui/graph-own-share.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/ui/judgment-history.js
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
workspaceDefinition,
|
|
15
15
|
workspaceDetailEmpty,
|
|
16
16
|
workspaceLedger,
|
|
17
|
-
workspaceMetrics,
|
|
18
17
|
workspaceNotice,
|
|
19
18
|
workspacePageHeader,
|
|
20
19
|
workspaceRailBlock,
|
|
@@ -1721,22 +1720,6 @@ export function createJudgmentHistoryUI({
|
|
|
1721
1720
|
? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
|
|
1722
1721
|
: '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
|
|
1723
1722
|
}));
|
|
1724
|
-
const stats = aggregate.stats ?? {};
|
|
1725
|
-
const metricCount = (value, suffix) => value === null || value === undefined ? '未確認' : `${value}${suffix}`;
|
|
1726
|
-
wrapper.append(workspaceMetrics(doc, normalMode() ? [
|
|
1727
|
-
{
|
|
1728
|
-
label: aggregate.query ? '表示対象' : aggregate.countComplete ? '判断件数' : '取得済み',
|
|
1729
|
-
value: metricCount(aggregate.countComplete ? stats.judgments : aggregate.retrievedCount, '件'),
|
|
1730
|
-
note: aggregate.countComplete ? 'この期間・絞り込みの全件数'
|
|
1731
|
-
: aggregate.query ? '取得済み履歴の検索結果・全件数未確認' : '全件数未確認',
|
|
1732
|
-
},
|
|
1733
|
-
{ label: '使った参照', value: metricCount(stats.references, '件'), note: '当時の記録にある一意の参照' },
|
|
1734
|
-
{ label: '確認済みの結果', value: metricCount(stats.confirmedOutcomes, '件'), note: '成果が確認済みと記録された判断' },
|
|
1735
|
-
] : [
|
|
1736
|
-
{ label: 'Brainbaseが代わりに判断した件数', value: metricCount(stats.judgments, '件'), note: 'この期間の履歴' },
|
|
1737
|
-
{ label: 'そこで使った参照', value: metricCount(stats.references, '件'), note: '代理判断に含まれる一意の参照' },
|
|
1738
|
-
{ label: '判断回数にすると', value: metricCount(stats.equivalent, '回分相当'), note: '代理判断1件を1回分として表示' },
|
|
1739
|
-
], { ariaLabel: '判断履歴の集計' }));
|
|
1740
1723
|
const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
|
|
1741
1724
|
? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
|
|
1742
1725
|
: 'この期間の判断履歴はありません。';
|
package/ui/local-web-shell.css
CHANGED
package/ui/local-web-shell.js
CHANGED
|
@@ -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
|
+
}
|
package/ui/outcome-knowledge.css
CHANGED
|
@@ -216,6 +216,11 @@
|
|
|
216
216
|
.knowledge-outcome .knowledge-form-actions,
|
|
217
217
|
.knowledge-outcome .knowledge-detail-actions,
|
|
218
218
|
.knowledge-outcome .knowledge-lifecycle-actions { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; }
|
|
219
|
+
.knowledge-outcome .knowledge-lifecycle-actions > .knowledge-revision-details {
|
|
220
|
+
flex: 1 1 100%;
|
|
221
|
+
min-width: 0;
|
|
222
|
+
}
|
|
223
|
+
.knowledge-outcome .knowledge-revision-details .knowledge-lifecycle-form { width: 100%; }
|
|
219
224
|
.knowledge-outcome .knowledge-filter-actions {
|
|
220
225
|
grid-column: 1 / -1;
|
|
221
226
|
align-self: end;
|
package/ui/outcome-knowledge.js
CHANGED
|
@@ -1168,7 +1168,7 @@ export function renderLifecycle(root, item, callbacks = {}) {
|
|
|
1168
1168
|
if (canEdit && callbacks.onRevision) {
|
|
1169
1169
|
const revisionDraft = callbacks.revisionDraft && typeof callbacks.revisionDraft === 'object' ? callbacks.revisionDraft : null;
|
|
1170
1170
|
const draftValue = (key, fallback) => revisionDraft && Object.prototype.hasOwnProperty.call(revisionDraft, key) ? revisionDraft[key] : fallback;
|
|
1171
|
-
const revisionDetails = makeElement('details', { className: 'knowledge-action-details' });
|
|
1171
|
+
const revisionDetails = makeElement('details', { className: 'knowledge-action-details knowledge-revision-details' });
|
|
1172
1172
|
const summary = makeElement('summary', { text: 'この判断を改訂' });
|
|
1173
1173
|
const form = makeElement('form', { className: 'knowledge-lifecycle-form' });
|
|
1174
1174
|
const title = makeElement('input', { className: 'knowledge-control', value: draftValue('title', item.title ?? ''), attrs: { name: 'title', maxlength: '240', required: 'required' } });
|
package/ui/workspace-kit.css
CHANGED
|
@@ -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; } }
|
package/ui/workspace-kit.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -1017,10 +1017,18 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
|
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
1019
|
let running = true;
|
|
1020
|
+
let lastSkyMinute = -1;
|
|
1020
1021
|
function frame(time) {
|
|
1021
1022
|
if (!running) return;
|
|
1022
1023
|
requestAnimationFrame(frame);
|
|
1023
1024
|
if (!open) return;
|
|
1025
|
+
// Use wall time so an open district also catches up after a suspended tab.
|
|
1026
|
+
const now = new Date();
|
|
1027
|
+
const minute = Math.floor(now.getTime() / 60_000);
|
|
1028
|
+
if (skyMode === 'auto' && minute !== lastSkyMinute) {
|
|
1029
|
+
if (skyAt(now.getHours(), 'auto').phase !== sky.phase) applySky();
|
|
1030
|
+
lastSkyMinute = minute;
|
|
1031
|
+
}
|
|
1024
1032
|
if (flight) {
|
|
1025
1033
|
flight.started ??= time;
|
|
1026
1034
|
const t = Math.min((time - flight.started) / flight.duration, 1);
|