@unson/brainbase-mcp 0.41.7 → 0.41.9
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/dist/foundation-graph-http.js +5 -1
- package/dist/graph-foundation-reader.d.ts +33 -4
- package/dist/graph-foundation-reader.js +107 -10
- package/package.json +2 -2
- package/ui/foundation-overview.css +1 -8
- package/ui/foundation-overview.js +168 -34
- package/ui/graph-own-share.js +28 -9
- package/ui/outcome-knowledge.css +18 -0
- package/ui/outcome-knowledge.js +265 -9
- package/ui/workspace-kit.css +42 -0
- package/ui/workspace-kit.js +165 -0
- package/ui/world/world-canvas-notes.js +2 -1
- package/ui/world/world-canvas-ui.css +48 -3
- package/ui/world/world-canvas-ui.js +17 -4
- package/ui/world/world-district.js +34 -5
- package/ui/world/world-view.css +6 -0
- package/ui/world/world-view.js +109 -19
- package/ui/world/world-work-rail.js +15 -2
package/ui/outcome-knowledge.js
CHANGED
|
@@ -380,6 +380,7 @@ export function normalizeKnowledgeItem(item) {
|
|
|
380
380
|
return {
|
|
381
381
|
id: fieldValue(item, 'id', 'knowledge_id', 'canonical_id') ?? null,
|
|
382
382
|
type: fieldValue(item, 'type', 'kind', 'entity_type') ?? null,
|
|
383
|
+
project_code: fieldValue(item, 'project_code', 'projectCode') ?? null,
|
|
383
384
|
title: fieldValue(item, 'title', 'name', 'subject') ?? null,
|
|
384
385
|
summary: fieldValue(item, 'summary', 'statement', 'body') ?? null,
|
|
385
386
|
content: fieldValue(item, 'content', 'canonical_content') ?? null,
|
|
@@ -411,12 +412,31 @@ export function normalizeKnowledgeCollection(payload) {
|
|
|
411
412
|
records,
|
|
412
413
|
absence_confirmed: absenceConfirmed,
|
|
413
414
|
searched_scope: Array.isArray(body.searched_scope) ? body.searched_scope : null,
|
|
415
|
+
truncated: body.truncated === true,
|
|
414
416
|
query: fieldValue(body, 'query', 'q') ?? null,
|
|
415
417
|
warnings: Array.isArray(body.warnings) ? body.warnings : null,
|
|
416
418
|
raw: payload,
|
|
417
419
|
};
|
|
418
420
|
}
|
|
419
421
|
|
|
422
|
+
const HEADING_MAX_LENGTH = 80;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The heading shown for a knowledge record. A record without a title uses
|
|
426
|
+
* the first line of its own summary or body instead of a generic label, so
|
|
427
|
+
* the list still tells records apart. Nothing is invented: when none of the
|
|
428
|
+
* three exists the heading says the title is unconfirmed.
|
|
429
|
+
*/
|
|
430
|
+
export function knowledgeItemHeading(item) {
|
|
431
|
+
const title = optionalText(item?.title).trim();
|
|
432
|
+
if (title) return title;
|
|
433
|
+
for (const value of [item?.summary, item?.content]) {
|
|
434
|
+
const firstLine = optionalText(value).split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
|
|
435
|
+
if (firstLine) return firstLine.length > HEADING_MAX_LENGTH ? `${firstLine.slice(0, HEADING_MAX_LENGTH)}…` : firstLine;
|
|
436
|
+
}
|
|
437
|
+
return 'タイトル未確認';
|
|
438
|
+
}
|
|
439
|
+
|
|
420
440
|
function isRecord(value) {
|
|
421
441
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
422
442
|
}
|
|
@@ -755,7 +775,8 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
|
|
|
755
775
|
queryLabel.append(query);
|
|
756
776
|
const scopeFieldLabel = makeElement('label', { text: '範囲' });
|
|
757
777
|
const scope = makeElement('select', { className: 'knowledge-control', attrs: { name: 'scope' } });
|
|
758
|
-
|
|
778
|
+
const scopeOptionLabel = (value) => optionalText(callbacks.scopeLabels?.[value]).trim() || scopeLabel(value);
|
|
779
|
+
for (const value of ['project', 'organization', 'all']) append(scope, makeElement('option', { text: scopeOptionLabel(value), value, attrs: { value, ...(filters.scope === value ? { selected: 'selected' } : {}) } }));
|
|
759
780
|
scope.value = filters.scope || 'all';
|
|
760
781
|
scopeFieldLabel.append(scope);
|
|
761
782
|
const statusLabel = makeElement('label', { text: '状態' });
|
|
@@ -781,6 +802,18 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
|
|
|
781
802
|
return form;
|
|
782
803
|
}
|
|
783
804
|
|
|
805
|
+
/**
|
|
806
|
+
* Where a record belongs, for lists that span several projects. Organization
|
|
807
|
+
* wide records say so instead of naming the project row that stores them.
|
|
808
|
+
*/
|
|
809
|
+
export function knowledgeItemProjectLabel(item, projectLabel) {
|
|
810
|
+
if (optionalText(item?.scope).trim().toLowerCase() === 'organization') return '組織共通';
|
|
811
|
+
const code = optionalText(item?.project_code).trim();
|
|
812
|
+
if (!code) return 'プロジェクト未確認';
|
|
813
|
+
const label = typeof projectLabel === 'function' ? optionalText(projectLabel(code)).trim() : '';
|
|
814
|
+
return label || code;
|
|
815
|
+
}
|
|
816
|
+
|
|
784
817
|
export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
785
818
|
clear(root);
|
|
786
819
|
const collection = model.records === undefined && (model.items || model.results || model.state) ? normalizeKnowledgeCollection(model) : model;
|
|
@@ -792,15 +825,24 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
|
792
825
|
&& ['ready', 'results', EMPTY].includes(state);
|
|
793
826
|
const section = makeElement('section', { className: 'knowledge-list-panel', attrs: { 'aria-labelledby': 'knowledge-list-title' } });
|
|
794
827
|
const header = makeElement('div', { className: 'knowledge-panel-head' });
|
|
795
|
-
|
|
796
|
-
|
|
828
|
+
const countText = countKnown
|
|
829
|
+
? `${collection.records.length}件${collection.truncated ? '(新しい順の上限まで)' : ''}`
|
|
830
|
+
: UNKNOWN;
|
|
831
|
+
const scopeText = typeof callbacks.scopeText === 'function'
|
|
832
|
+
? callbacks.scopeText(collection)
|
|
833
|
+
: searchedScope ? `検索範囲: ${searchedScope}` : '検索範囲: 未確認';
|
|
834
|
+
append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text: countText }));
|
|
835
|
+
header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: scopeText }));
|
|
836
|
+
if (collection.truncated && countKnown) {
|
|
837
|
+
header.firstChild.append(makeElement('p', { className: 'knowledge-list-truncated', text: '表示は新しい順の上限までです。キーワードやプロジェクトで絞ると、ほかの記録も見られます。' }));
|
|
838
|
+
}
|
|
797
839
|
section.append(header);
|
|
798
840
|
if (state === 'loading') {
|
|
799
841
|
const skeleton = makeElement('div', { className: 'knowledge-list-rows', attrs: { 'aria-label': '知識を読み込み中' } });
|
|
800
842
|
for (let index = 0; index < 4; index += 1) skeleton.append(skeletonRow());
|
|
801
843
|
section.append(skeleton);
|
|
802
844
|
} else if (state === EMPTY) {
|
|
803
|
-
section.append(makeElement('div', { className: 'knowledge-empty', text: 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
|
|
845
|
+
section.append(makeElement('div', { className: 'knowledge-empty', text: callbacks.emptyText ?? 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
|
|
804
846
|
} else if (state === 'permission_denied') {
|
|
805
847
|
// Keep the upstream reason and recovery action visible. Replacing the
|
|
806
848
|
// payload with a generic detail makes a denied scope look like an
|
|
@@ -818,13 +860,14 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
|
818
860
|
const hasSelectedId = callbacks.selectedId !== undefined && callbacks.selectedId !== null;
|
|
819
861
|
if (!collection.records.length) rows.append(makeElement('div', { className: 'knowledge-empty', text: '検索結果を確認できません。空の一覧とは扱いません。' }));
|
|
820
862
|
for (const item of collection.records) {
|
|
821
|
-
const title =
|
|
863
|
+
const title = knowledgeItemHeading(item);
|
|
822
864
|
const row = makeElement('button', { className: 'knowledge-list-row', attrs: { type: 'button', 'aria-pressed': String(item.id === callbacks.selectedId), 'aria-controls': 'knowledge-selected-detail', title } });
|
|
823
865
|
const main = makeElement('span', { className: 'knowledge-row-main' });
|
|
824
866
|
const meta = makeElement('span', { className: 'knowledge-row-meta' });
|
|
867
|
+
if (callbacks.showProject) meta.append(makeElement('span', { className: 'knowledge-row-project', text: knowledgeItemProjectLabel(item, callbacks.projectLabel) }));
|
|
825
868
|
append(meta,
|
|
826
869
|
makeElement('span', { className: 'knowledge-row-type', text: item.type ? typeLabel(item.type) : UNKNOWN }),
|
|
827
|
-
makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
|
|
870
|
+
callbacks.showProject ? null : makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
|
|
828
871
|
makeElement('span', { className: 'knowledge-row-state', text: item.lifecycle.status !== UNKNOWN ? lifecycleStatusLabel(item.lifecycle.status) : UNKNOWN }),
|
|
829
872
|
);
|
|
830
873
|
append(main,
|
|
@@ -895,14 +938,14 @@ export function renderKnowledgeDetail(root, item, callbacks = {}) {
|
|
|
895
938
|
}
|
|
896
939
|
append(section,
|
|
897
940
|
makeElement('span', { className: 'knowledge-eyebrow', text: item.type ? typeLabel(item.type) : UNKNOWN }),
|
|
898
|
-
makeElement('h3', { text:
|
|
941
|
+
makeElement('h3', { text: knowledgeItemHeading(item), attrs: { id: 'knowledge-detail-title' } }),
|
|
899
942
|
);
|
|
900
943
|
const summary = makeElement('p', { className: 'knowledge-long-text', text: item.summary ? text(item.summary) : '要旨未確認' });
|
|
901
944
|
section.append(summary);
|
|
902
945
|
renderDefinitionList(section, [
|
|
903
946
|
['正本ID', item.id ? text(item.id) : UNKNOWN],
|
|
904
947
|
['版', item.version !== null && item.version !== undefined && String(item.version).trim() !== '' ? String(item.version) : UNKNOWN],
|
|
905
|
-
['範囲', item.scope ? scopeLabel(item.scope) : UNKNOWN],
|
|
948
|
+
['範囲', item.scope ? (optionalText(callbacks.scopeLabels?.[optionalText(item.scope).trim().toLowerCase()]).trim() || scopeLabel(item.scope)) : UNKNOWN],
|
|
906
949
|
['責任者', ownerValue(item.owner)],
|
|
907
950
|
['有効状態', lifecycleStatusLabel(item.lifecycle.status)],
|
|
908
951
|
['適用', item.applicability.state ? applicabilityStateLabel(item.applicability.state) : UNKNOWN],
|
|
@@ -1383,6 +1426,212 @@ function defaultCandidateBody(values, context = {}) {
|
|
|
1383
1426
|
};
|
|
1384
1427
|
}
|
|
1385
1428
|
|
|
1429
|
+
function projectOptions(projects) {
|
|
1430
|
+
const list = typeof projects === 'function' ? projects() : projects;
|
|
1431
|
+
if (!Array.isArray(list)) return [];
|
|
1432
|
+
const options = [];
|
|
1433
|
+
for (const project of list) {
|
|
1434
|
+
const code = optionalText(fieldValue(project, 'code', 'project_code', 'projectCode') ?? (typeof project === 'string' ? project : '')).trim();
|
|
1435
|
+
if (!code || options.some((option) => option.code === code)) continue;
|
|
1436
|
+
const name = optionalText(fieldValue(project, 'name', 'display_name', 'displayName', 'label')).trim();
|
|
1437
|
+
options.push({ code, name: name || code });
|
|
1438
|
+
}
|
|
1439
|
+
return options;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Renders the optional project filter shared by the organization-wide list
|
|
1444
|
+
* and the single project workspace. The empty value means every project the
|
|
1445
|
+
* viewer may read; it never stands for an implicitly chosen project.
|
|
1446
|
+
*/
|
|
1447
|
+
export function renderKnowledgeProjectFilter(root, model = {}, callbacks = {}) {
|
|
1448
|
+
clear(root);
|
|
1449
|
+
const label = makeElement('label', { className: 'knowledge-project-filter', text: 'プロジェクト' });
|
|
1450
|
+
const select = makeElement('select', { className: 'knowledge-control', attrs: { name: 'knowledge_project', 'aria-label': 'プロジェクトで絞り込む' } });
|
|
1451
|
+
const selected = optionalText(model.selected).trim();
|
|
1452
|
+
const options = projectOptions(model.projects);
|
|
1453
|
+
append(select, makeElement('option', { text: 'すべてのプロジェクト', value: '', attrs: { value: '', ...(selected ? {} : { selected: 'selected' }) } }));
|
|
1454
|
+
for (const option of options) {
|
|
1455
|
+
append(select, makeElement('option', { text: option.name === option.code ? option.code : `${option.name}(${option.code})`, value: option.code, attrs: { value: option.code, ...(option.code === selected ? { selected: 'selected' } : {}) } }));
|
|
1456
|
+
}
|
|
1457
|
+
if (selected && !options.some((option) => option.code === selected)) {
|
|
1458
|
+
append(select, makeElement('option', { text: selected, value: selected, attrs: { value: selected, selected: 'selected' } }));
|
|
1459
|
+
}
|
|
1460
|
+
select.value = selected;
|
|
1461
|
+
select.addEventListener('change', () => callbacks.onChange?.(optionalText(select.value).trim() || null));
|
|
1462
|
+
label.append(select);
|
|
1463
|
+
root.append(label);
|
|
1464
|
+
return label;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Creates the organization-wide knowledge list. It reads every project the
|
|
1469
|
+
* viewer may read plus organization-wide records from one host path, shows
|
|
1470
|
+
* the project each record belongs to, and only reads. Registration and
|
|
1471
|
+
* version changes stay in the single project workspace: choosing a project
|
|
1472
|
+
* calls `onSelectProject`, and the host mounts `createKnowledgeOutcomeController`.
|
|
1473
|
+
*
|
|
1474
|
+
* @param {object} options
|
|
1475
|
+
* @param {string} [options.itemsPath] Host path for the organization-wide list.
|
|
1476
|
+
* @param {Array|Function} [options.projects] Projects offered in the filter.
|
|
1477
|
+
* @param {(code: string|null, context?: object) => void} [options.onSelectProject]
|
|
1478
|
+
*/
|
|
1479
|
+
export function createKnowledgeOverviewController(options = {}) {
|
|
1480
|
+
const { root, api, onSelectProject } = options;
|
|
1481
|
+
if (!root || typeof root.replaceChildren !== 'function') throw new Error('knowledge_root_unavailable');
|
|
1482
|
+
const labels = resolveKnowledgeUiLabels(options.labels);
|
|
1483
|
+
const itemsBase = optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`;
|
|
1484
|
+
const state = {
|
|
1485
|
+
filters: { q: '', scope: 'all', status: 'active' },
|
|
1486
|
+
list: { state: 'loading', records: null, absence_confirmed: false, searched_scope: null },
|
|
1487
|
+
selected: null,
|
|
1488
|
+
detail: { state: 'idle' },
|
|
1489
|
+
};
|
|
1490
|
+
let selectionGeneration = 0;
|
|
1491
|
+
const projectName = (code) => projectOptions(options.projects).find((option) => option.code === code)?.name ?? null;
|
|
1492
|
+
|
|
1493
|
+
function itemsPath() {
|
|
1494
|
+
const query = new URLSearchParams();
|
|
1495
|
+
if (state.filters.q) query.set('q', state.filters.q);
|
|
1496
|
+
if (state.filters.scope) query.set('scope', state.filters.scope);
|
|
1497
|
+
if (state.filters.status) query.set('status', state.filters.status);
|
|
1498
|
+
const encoded = query.toString();
|
|
1499
|
+
return `${itemsBase}${encoded ? `?${encoded}` : ''}`;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function emptyText() {
|
|
1503
|
+
const filtered = Boolean(state.filters.q) || state.filters.scope !== 'all' || state.filters.status !== 'active';
|
|
1504
|
+
return filtered
|
|
1505
|
+
? 'この条件に一致する知識・判断はありません。'
|
|
1506
|
+
: '見られる範囲に、有効な知識・判断の記録はありません。';
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function scopeText(collection) {
|
|
1510
|
+
const count = Array.isArray(collection.searched_scope) ? collection.searched_scope.length : null;
|
|
1511
|
+
return count === null
|
|
1512
|
+
? '読んだ範囲: 未確認'
|
|
1513
|
+
: `読んだ範囲: 見られる${count}件のプロジェクトと組織共通`;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function openProject(code, context = {}) {
|
|
1517
|
+
if (!code) return;
|
|
1518
|
+
onSelectProject?.(code, context);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function render() {
|
|
1522
|
+
clear(root);
|
|
1523
|
+
const shell = makeElement('div', { className: 'knowledge-outcome knowledge-overview' });
|
|
1524
|
+
const header = makeElement('header', { className: 'knowledge-outcome-head' });
|
|
1525
|
+
const headerMain = makeElement('div', { className: 'knowledge-header-main' });
|
|
1526
|
+
append(headerMain,
|
|
1527
|
+
makeElement('span', { className: 'knowledge-eyebrow', text: 'Brainbase Outcome' }),
|
|
1528
|
+
makeElement('h2', { text: '知識・判断' }),
|
|
1529
|
+
makeElement('p', { text: '見られるすべてのプロジェクトと組織共通の知識・判断を一覧にします。登録や改訂は、プロジェクトを選ぶとできます。' }),
|
|
1530
|
+
);
|
|
1531
|
+
const headerMeta = makeElement('div', { className: 'knowledge-header-meta' });
|
|
1532
|
+
renderKnowledgeProjectFilter(headerMeta, { projects: options.projects, selected: null }, { onChange: (code) => openProject(code, { reason: 'filter' }) });
|
|
1533
|
+
append(header, headerMain, headerMeta);
|
|
1534
|
+
shell.append(header);
|
|
1535
|
+
const body = makeElement('div', { className: 'knowledge-outcome-body knowledge-workspace', attrs: { 'data-view': 'discovery' } });
|
|
1536
|
+
const primary = makeElement('section', { className: 'knowledge-outcome-primary knowledge-master', attrs: { 'aria-label': '知識の一覧' } });
|
|
1537
|
+
const discovery = makeElement('section', { className: 'knowledge-discovery-view', attrs: { 'aria-labelledby': 'knowledge-discovery-title' } });
|
|
1538
|
+
const discoveryHeader = makeElement('div', { className: 'knowledge-view-heading' });
|
|
1539
|
+
append(discoveryHeader, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: labels.discovery }));
|
|
1540
|
+
discoveryHeader.firstChild.append(makeElement('h3', { id: 'knowledge-discovery-title', text: '知識・判断を探す' }), makeElement('p', { text: '選んだ記録の本文・出典・所属プロジェクトを確認できます。' }));
|
|
1541
|
+
discovery.append(discoveryHeader);
|
|
1542
|
+
const filters = makeElement('div', { className: 'knowledge-filter-region knowledge-discovery-toolbar' });
|
|
1543
|
+
renderDiscoveryFilters(filters, state.filters, {
|
|
1544
|
+
scopeLabels: { project: 'プロジェクトの記録', organization: '組織共通', all: 'すべて' },
|
|
1545
|
+
onChange: (next) => { state.filters = next; void loadItems(); },
|
|
1546
|
+
});
|
|
1547
|
+
discovery.append(filters);
|
|
1548
|
+
const list = makeElement('div', { className: 'knowledge-discovery-list' });
|
|
1549
|
+
renderKnowledgeList(list, state.list, {
|
|
1550
|
+
labels,
|
|
1551
|
+
showProject: true,
|
|
1552
|
+
projectLabel: projectName,
|
|
1553
|
+
scopeText,
|
|
1554
|
+
emptyText: emptyText(),
|
|
1555
|
+
selectedId: state.selected?.id,
|
|
1556
|
+
onSelect: (item) => { void selectItem(item); },
|
|
1557
|
+
onRetry: () => loadItems(),
|
|
1558
|
+
});
|
|
1559
|
+
discovery.append(list);
|
|
1560
|
+
primary.append(discovery);
|
|
1561
|
+
body.append(primary);
|
|
1562
|
+
const detail = makeElement('aside', { className: 'knowledge-outcome-detail knowledge-inspector', attrs: { 'aria-label': labels.inspector, id: 'knowledge-selected-detail', tabindex: '-1' } });
|
|
1563
|
+
const inspectorHeading = makeElement('div', { className: 'knowledge-inspector-heading' });
|
|
1564
|
+
append(inspectorHeading, makeElement('span', { className: 'knowledge-view-kicker', text: labels.inspector }), makeElement('span', { className: 'knowledge-inspector-state', text: state.selected ? labels.selected : labels.unselected }));
|
|
1565
|
+
detail.append(inspectorHeading);
|
|
1566
|
+
const detailContent = makeElement('div', { className: 'knowledge-inspector-detail' });
|
|
1567
|
+
const hasSelectableKnowledge = normaliseStatus(state.list.state) === 'ready' && Array.isArray(state.list.records) && state.list.records.length > 0;
|
|
1568
|
+
renderKnowledgeDetail(detailContent, state.selected, {
|
|
1569
|
+
labels,
|
|
1570
|
+
detailState: state.detail,
|
|
1571
|
+
scopeLabels: { project: 'プロジェクトの記録', organization: '組織共通' },
|
|
1572
|
+
showSelectionGuide: Boolean(state.selected) || hasSelectableKnowledge,
|
|
1573
|
+
onRetry: state.selected ? () => void selectItem(state.selected) : null,
|
|
1574
|
+
});
|
|
1575
|
+
detail.append(detailContent);
|
|
1576
|
+
if (state.selected) {
|
|
1577
|
+
const owner = makeElement('div', { className: 'knowledge-overview-owner' });
|
|
1578
|
+
const code = optionalText(state.selected.project_code).trim();
|
|
1579
|
+
append(owner, makeElement('p', { className: 'knowledge-overview-project', text: `所属: ${knowledgeItemProjectLabel(state.selected, projectName)}` }));
|
|
1580
|
+
if (code) {
|
|
1581
|
+
const open = makeElement('button', { className: 'knowledge-button secondary', text: 'このプロジェクトで開く', attrs: { type: 'button' } });
|
|
1582
|
+
open.addEventListener('click', () => openProject(code, { reason: 'record', item_id: state.selected.id }));
|
|
1583
|
+
owner.append(open);
|
|
1584
|
+
}
|
|
1585
|
+
detail.append(owner);
|
|
1586
|
+
}
|
|
1587
|
+
body.append(detail); shell.append(body); root.append(shell);
|
|
1588
|
+
options.onStateChange?.(state);
|
|
1589
|
+
return shell;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
async function loadItems() {
|
|
1593
|
+
selectionGeneration += 1;
|
|
1594
|
+
state.selected = null;
|
|
1595
|
+
state.detail = { state: 'idle' };
|
|
1596
|
+
state.list = { ...state.list, state: 'loading' }; render();
|
|
1597
|
+
try { state.list = normalizeKnowledgeCollection(await callKnowledgeApi(api, itemsPath())); }
|
|
1598
|
+
catch (error) { state.list = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', records: null, message: apiErrorText(error), error: error.code }; }
|
|
1599
|
+
render(); return state.list;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async function selectItem(item) {
|
|
1603
|
+
const requestGeneration = ++selectionGeneration;
|
|
1604
|
+
state.selected = item ?? null;
|
|
1605
|
+
const code = optionalText(item?.project_code).trim();
|
|
1606
|
+
if (!item?.id || !code) {
|
|
1607
|
+
// A record whose project is unknown cannot be read through a project
|
|
1608
|
+
// route. Show what the list returned and say the canonical read is
|
|
1609
|
+
// unavailable instead of presenting it as the confirmed detail.
|
|
1610
|
+
state.detail = item ? { state: 'unknown', message: '所属プロジェクトを確認できないため、正本の詳細を読めません。' } : { state: 'idle' };
|
|
1611
|
+
render(); return null;
|
|
1612
|
+
}
|
|
1613
|
+
state.detail = { state: 'loading' }; render();
|
|
1614
|
+
try {
|
|
1615
|
+
const path = typeof options.detailPath === 'function'
|
|
1616
|
+
? options.detailPath(code, item.id)
|
|
1617
|
+
: knowledgePath(code, `${KNOWLEDGE_ROUTES.items}/${encodeURIComponent(item.id)}`);
|
|
1618
|
+
const payload = await callKnowledgeApi(api, path);
|
|
1619
|
+
if (requestGeneration !== selectionGeneration) return null;
|
|
1620
|
+
const read = normalizeKnowledgeItem(payload?.item ?? payload?.record ?? payload?.data ?? payload);
|
|
1621
|
+
const detailed = read ? { ...read, project_code: read.project_code ?? code } : item;
|
|
1622
|
+
state.selected = detailed; state.detail = { state: 'ready', item: detailed }; render(); return detailed;
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
if (requestGeneration !== selectionGeneration) return null;
|
|
1625
|
+
state.detail = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code }; render(); return null;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const controller = { state, render, loadItems, selectItem, paths: { items: itemsPath } };
|
|
1630
|
+
render();
|
|
1631
|
+
if (options.autoLoad !== false) void loadItems();
|
|
1632
|
+
return controller;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1386
1635
|
/**
|
|
1387
1636
|
* Creates the knowledge outcome workspace.
|
|
1388
1637
|
*
|
|
@@ -1452,7 +1701,14 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1452
1701
|
makeElement('p', { text: '正本を探し、由来と状態を確認して、必要なときだけ更新します。' }),
|
|
1453
1702
|
);
|
|
1454
1703
|
const headerMeta = makeElement('div', { className: 'knowledge-header-meta' });
|
|
1455
|
-
|
|
1704
|
+
if (options.projectFilter) {
|
|
1705
|
+
// The host decides what choosing another project, or every project,
|
|
1706
|
+
// means. Showing the filter keeps the chosen project explicit instead
|
|
1707
|
+
// of a fixed label that cannot be changed from here.
|
|
1708
|
+
renderKnowledgeProjectFilter(headerMeta, { projects: options.projectFilter.projects, selected: code }, { onChange: (next) => options.projectFilter.onChange?.(next) });
|
|
1709
|
+
} else {
|
|
1710
|
+
headerMeta.append(makeElement('span', { className: 'knowledge-project-context', text: `プロジェクト: ${code}` }));
|
|
1711
|
+
}
|
|
1456
1712
|
append(header, headerMain, headerMeta);
|
|
1457
1713
|
const nav = makeElement('nav', { className: 'knowledge-outcome-nav', attrs: { 'aria-label': '知識の操作' } });
|
|
1458
1714
|
for (const [value, label] of [['discovery', '探す'], ['capture', '登録案を作る'], ['preview', '公開前テスト'], ['destination', '保存先']]) {
|
package/ui/workspace-kit.css
CHANGED
|
@@ -573,3 +573,45 @@
|
|
|
573
573
|
.bb-ws-drawer[open], .bb-ws-drawer.is-closing, .bb-ws-drawer.is-closing::backdrop,
|
|
574
574
|
.bb-ws-filter-popover, .bb-ws-loading-spinner::before, .bb-ws-error-card.is-dismissing { animation: none; }
|
|
575
575
|
}
|
|
576
|
+
|
|
577
|
+
/* Searchable dropdown (workspaceCombobox) */
|
|
578
|
+
.bb-ws-combobox { position: relative; display: inline-grid; min-width: 0; }
|
|
579
|
+
.bb-ws-combobox-trigger {
|
|
580
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--bb-space-2);
|
|
581
|
+
width: 100%; min-width: 0; min-height: 38px; box-sizing: border-box; padding: 0 10px 0 11px;
|
|
582
|
+
border: 1px solid var(--bb-color-border-strong); border-radius: var(--bb-radius-sm);
|
|
583
|
+
background: var(--bb-color-panel); color: var(--bb-color-ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer;
|
|
584
|
+
}
|
|
585
|
+
.bb-ws-combobox-trigger:hover { border-color: var(--bb-color-muted); }
|
|
586
|
+
.bb-ws-combobox-trigger:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 1px; }
|
|
587
|
+
.bb-ws-combobox-trigger[aria-expanded='true'] { border-color: var(--bb-color-accent); }
|
|
588
|
+
.bb-ws-combobox-trigger[disabled] { color: var(--bb-color-quiet); cursor: not-allowed; }
|
|
589
|
+
.bb-ws-combobox-trigger[data-empty='true'] .bb-ws-combobox-value { color: var(--bb-color-quiet); }
|
|
590
|
+
.bb-ws-combobox-value { overflow: hidden; min-width: 0; text-overflow: ellipsis; white-space: nowrap; }
|
|
591
|
+
.bb-ws-combobox-chevron::after { content: '⌄'; color: var(--bb-color-muted); }
|
|
592
|
+
.bb-ws-combobox-popup {
|
|
593
|
+
position: absolute; z-index: 45; top: calc(100% + 6px); left: 0; display: grid; gap: var(--bb-space-2);
|
|
594
|
+
box-sizing: border-box; width: max(100%, 260px); max-width: calc(100vw - 32px); padding: 8px;
|
|
595
|
+
border: 1px solid var(--bb-color-border); border-radius: var(--bb-radius-md); background: var(--bb-color-panel); color: var(--bb-color-ink);
|
|
596
|
+
box-shadow: var(--bb-shadow-popover); animation: bb-ws-filter-enter 140ms ease-out; transform-origin: top left;
|
|
597
|
+
}
|
|
598
|
+
.bb-ws-combobox-popup[hidden], .bb-ws-combobox-search[hidden] { display: none !important; }
|
|
599
|
+
.bb-ws-combobox-search {
|
|
600
|
+
box-sizing: border-box; width: 100%; min-height: 34px; padding: 0 10px;
|
|
601
|
+
border: 1px solid var(--bb-color-border-strong); border-radius: var(--bb-radius-sm); background: var(--bb-color-surface); color: var(--bb-color-ink); font: inherit; font-size: 13px;
|
|
602
|
+
}
|
|
603
|
+
.bb-ws-combobox-search:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 0; }
|
|
604
|
+
.bb-ws-combobox-list { max-height: 300px; margin: 0; padding: 0; overflow-y: auto; list-style: none; }
|
|
605
|
+
.bb-ws-combobox-list:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 0; }
|
|
606
|
+
.bb-ws-combobox-option {
|
|
607
|
+
display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; align-items: center; gap: var(--bb-space-2);
|
|
608
|
+
padding: 7px 8px; border-radius: var(--bb-radius-sm); font-size: 13px; cursor: pointer;
|
|
609
|
+
}
|
|
610
|
+
.bb-ws-combobox-option::before { content: ''; color: var(--bb-color-accent-strong); font-weight: 700; }
|
|
611
|
+
.bb-ws-combobox-option[aria-selected='true']::before { content: '✓'; }
|
|
612
|
+
.bb-ws-combobox-option:hover, .bb-ws-combobox-option.is-active { background: var(--bb-color-accent-soft); }
|
|
613
|
+
.bb-ws-combobox-option.is-active { box-shadow: inset 2px 0 0 var(--bb-color-accent); }
|
|
614
|
+
.bb-ws-combobox-option-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
615
|
+
.bb-ws-combobox-option-hint { color: var(--bb-color-muted); font-size: 12px; white-space: nowrap; }
|
|
616
|
+
.bb-ws-combobox-status { margin: 0; padding: 2px 4px 0; color: var(--bb-color-muted); font-size: 12px; }
|
|
617
|
+
@media (prefers-reduced-motion:reduce) { .bb-ws-combobox-popup { animation: none; } }
|
package/ui/workspace-kit.js
CHANGED
|
@@ -422,3 +422,168 @@ export function workspaceDisclosure(doc, { title, help, content, open = false, o
|
|
|
422
422
|
if (onToggle) details.addEventListener('toggle', () => onToggle(details.open));
|
|
423
423
|
return details;
|
|
424
424
|
}
|
|
425
|
+
|
|
426
|
+
function comboboxText(value) {
|
|
427
|
+
return String(value ?? '').normalize('NFKC').toLocaleLowerCase('ja');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Dropdown whose list can be searched. The trigger shows the chosen label;
|
|
432
|
+
* opening it moves focus to a search box (role=combobox) that narrows a
|
|
433
|
+
* listbox. Arrow keys move the active option, Enter chooses it, Escape closes
|
|
434
|
+
* and returns focus to the trigger, and typing on the trigger starts a search.
|
|
435
|
+
* Long lists render the first `limit` matches and say how many the search
|
|
436
|
+
* would still reveal. Options are `{ value, label, hint?, keywords? }`; the
|
|
437
|
+
* host owns the chosen value and receives it through `onChange`.
|
|
438
|
+
*/
|
|
439
|
+
export function workspaceCombobox(doc, {
|
|
440
|
+
label, options = [], value = '', placeholder = '選択してください', searchLabel, searchPlaceholder = '絞り込む',
|
|
441
|
+
emptyText = '一致する候補はありません', limit = 100, searchable = 'auto', className, onChange,
|
|
442
|
+
} = {}) {
|
|
443
|
+
const sequence = ++workspaceSequence;
|
|
444
|
+
const listId = `bb-ws-combobox-${sequence}-list`;
|
|
445
|
+
const root = el(doc, 'span', { className: ['bb-ws-combobox', className].filter(Boolean).join(' ') });
|
|
446
|
+
const trigger = el(doc, 'button', { className: 'bb-ws-combobox-trigger', attrs: { type: 'button', 'aria-haspopup': 'listbox', 'aria-expanded': 'false', 'aria-controls': listId } });
|
|
447
|
+
const triggerText = el(doc, 'span', { className: 'bb-ws-combobox-value' });
|
|
448
|
+
trigger.append(triggerText, el(doc, 'span', { className: 'bb-ws-combobox-chevron', attrs: { 'aria-hidden': 'true' } }));
|
|
449
|
+
const popup = el(doc, 'div', { className: 'bb-ws-combobox-popup' });
|
|
450
|
+
popup.hidden = true;
|
|
451
|
+
const search = el(doc, 'input', { className: 'bb-ws-combobox-search', attrs: {
|
|
452
|
+
type: 'text', role: 'combobox', 'aria-autocomplete': 'list', 'aria-expanded': 'true', 'aria-controls': listId,
|
|
453
|
+
'aria-label': searchLabel || `${label || '候補'}を絞り込む`, placeholder: searchPlaceholder, autocomplete: 'off', spellcheck: 'false',
|
|
454
|
+
} });
|
|
455
|
+
const list = el(doc, 'ul', { className: 'bb-ws-combobox-list', attrs: { id: listId, role: 'listbox', 'aria-label': label || '候補', tabindex: '-1' } });
|
|
456
|
+
const status = el(doc, 'p', { className: 'bb-ws-combobox-status', attrs: { role: 'status', 'aria-live': 'polite' } });
|
|
457
|
+
popup.append(search, list, status);
|
|
458
|
+
root.append(trigger, popup);
|
|
459
|
+
|
|
460
|
+
const state = { options: [], value: String(value ?? ''), query: '', active: -1, shown: [], open: false, disabled: false };
|
|
461
|
+
const usesSearch = () => searchable === true || (searchable === 'auto' && state.options.length > 8);
|
|
462
|
+
const focusTarget = () => (usesSearch() ? search : list);
|
|
463
|
+
const chosen = () => state.options.find(option => option.value === state.value);
|
|
464
|
+
|
|
465
|
+
function setOptions(next) {
|
|
466
|
+
state.options = (Array.isArray(next) ? next : []).map(option => ({
|
|
467
|
+
value: String(option?.value ?? ''), label: String(option?.label ?? option?.value ?? ''),
|
|
468
|
+
hint: option?.hint ? String(option.hint) : '', keywords: option?.keywords ? String(option.keywords) : '',
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderTrigger() {
|
|
473
|
+
const option = chosen();
|
|
474
|
+
// A value the list does not offer stays visible instead of looking unset.
|
|
475
|
+
const text = option ? option.label : state.value || placeholder;
|
|
476
|
+
triggerText.textContent = text;
|
|
477
|
+
trigger.setAttribute('aria-label', `${label ? `${label}: ` : ''}${text}`);
|
|
478
|
+
trigger.setAttribute('data-empty', String(!option && !state.value));
|
|
479
|
+
if (state.disabled) trigger.setAttribute('disabled', ''); else trigger.removeAttribute('disabled');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function activeDescendant(node) {
|
|
483
|
+
for (const target of [search, list]) {
|
|
484
|
+
if (node) target.setAttribute('aria-activedescendant', node.getAttribute('id'));
|
|
485
|
+
else target.removeAttribute('aria-activedescendant');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function renderList() {
|
|
490
|
+
const terms = comboboxText(state.query).split(/\s+/u).filter(Boolean);
|
|
491
|
+
const matches = terms.length
|
|
492
|
+
? state.options.filter(option => { const text = comboboxText(`${option.label}\n${option.hint}\n${option.keywords}`); return terms.every(term => text.includes(term)); })
|
|
493
|
+
: state.options;
|
|
494
|
+
const max = Math.max(1, Number(limit) || 100);
|
|
495
|
+
state.shown = matches.slice(0, max);
|
|
496
|
+
if (state.active >= state.shown.length) state.active = state.shown.length - 1;
|
|
497
|
+
list.replaceChildren();
|
|
498
|
+
state.shown.forEach((option, index) => {
|
|
499
|
+
const selected = option.value === state.value;
|
|
500
|
+
const item = el(doc, 'li', { className: `bb-ws-combobox-option${index === state.active ? ' is-active' : ''}`, attrs: { id: `bb-ws-combobox-${sequence}-option-${index}`, role: 'option', 'aria-selected': String(selected), 'data-value': option.value } });
|
|
501
|
+
item.append(el(doc, 'span', { className: 'bb-ws-combobox-option-label', text: option.label }));
|
|
502
|
+
if (option.hint) item.append(el(doc, 'span', { className: 'bb-ws-combobox-option-hint', text: option.hint }));
|
|
503
|
+
item.addEventListener('pointerdown', event => event.preventDefault?.());
|
|
504
|
+
item.addEventListener('click', () => choose(option));
|
|
505
|
+
list.append(item);
|
|
506
|
+
});
|
|
507
|
+
const active = list.children?.[state.active];
|
|
508
|
+
activeDescendant(active);
|
|
509
|
+
active?.scrollIntoView?.({ block: 'nearest' });
|
|
510
|
+
const rest = matches.length - state.shown.length;
|
|
511
|
+
status.textContent = !matches.length ? emptyText : rest > 0 ? `${matches.length}件中${state.shown.length}件を表示。ほか${rest}件は絞り込むと出ます` : `${matches.length}件`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function move(index) {
|
|
515
|
+
if (!state.shown.length) return;
|
|
516
|
+
state.active = Math.min(state.shown.length - 1, Math.max(0, index));
|
|
517
|
+
renderList();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function removeListeners() { doc.removeEventListener?.('pointerdown', outside); }
|
|
521
|
+
function outside(event) { if (!root.contains?.(event.target)) close(false); }
|
|
522
|
+
|
|
523
|
+
function open(query = '') {
|
|
524
|
+
if (state.disabled) return;
|
|
525
|
+
if (!state.open) { state.open = true; doc.addEventListener?.('pointerdown', outside); }
|
|
526
|
+
state.query = query; search.value = query; search.hidden = !usesSearch();
|
|
527
|
+
const selectedIndex = state.options.findIndex(option => option.value === state.value);
|
|
528
|
+
state.active = query ? 0 : Math.max(0, selectedIndex);
|
|
529
|
+
popup.hidden = false; trigger.setAttribute('aria-expanded', 'true');
|
|
530
|
+
renderList();
|
|
531
|
+
focusTarget().focus?.();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function close(returnFocus = true) {
|
|
535
|
+
if (!state.open) return;
|
|
536
|
+
state.open = false; removeListeners();
|
|
537
|
+
popup.hidden = true; trigger.setAttribute('aria-expanded', 'false'); activeDescendant(null);
|
|
538
|
+
if (returnFocus) trigger.focus?.();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function choose(option) {
|
|
542
|
+
const changed = option.value !== state.value;
|
|
543
|
+
state.value = option.value; renderTrigger(); close(true);
|
|
544
|
+
if (changed) onChange?.(option.value, option);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function keydown(event) {
|
|
548
|
+
const page = 10;
|
|
549
|
+
const keys = {
|
|
550
|
+
ArrowDown: () => move(state.active + 1), ArrowUp: () => move(state.active - 1),
|
|
551
|
+
PageDown: () => move(state.active + page), PageUp: () => move(state.active - page),
|
|
552
|
+
Enter: () => { const option = state.shown[state.active]; if (option) choose(option); },
|
|
553
|
+
Escape: () => close(true),
|
|
554
|
+
};
|
|
555
|
+
// Home and End move the caret while a search box is shown.
|
|
556
|
+
if (!usesSearch()) Object.assign(keys, { Home: () => move(0), End: () => move(state.shown.length - 1) });
|
|
557
|
+
if (event.key === 'Tab') { close(false); return; }
|
|
558
|
+
const action = keys[event.key];
|
|
559
|
+
if (!action) return;
|
|
560
|
+
event.preventDefault?.(); event.stopPropagation?.();
|
|
561
|
+
action();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
trigger.addEventListener('click', () => (state.open ? close(true) : open()));
|
|
565
|
+
trigger.addEventListener('keydown', event => {
|
|
566
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault?.(); open(); return; }
|
|
567
|
+
const printable = typeof event.key === 'string' && event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
568
|
+
if (printable && usesSearch()) { event.preventDefault?.(); open(event.key); }
|
|
569
|
+
});
|
|
570
|
+
search.addEventListener('input', () => { state.query = search.value; state.active = 0; renderList(); });
|
|
571
|
+
search.addEventListener('keydown', keydown);
|
|
572
|
+
list.addEventListener('keydown', keydown);
|
|
573
|
+
|
|
574
|
+
setOptions(options); renderTrigger();
|
|
575
|
+
return {
|
|
576
|
+
root, trigger, popup, search, list,
|
|
577
|
+
get value() { return state.value; },
|
|
578
|
+
get isOpen() { return state.open; },
|
|
579
|
+
open, close,
|
|
580
|
+
update({ options: nextOptions, value: nextValue, disabled } = {}) {
|
|
581
|
+
if (nextOptions !== undefined) setOptions(nextOptions);
|
|
582
|
+
if (nextValue !== undefined) state.value = String(nextValue ?? '');
|
|
583
|
+
if (disabled !== undefined) { state.disabled = Boolean(disabled); if (state.disabled) close(false); }
|
|
584
|
+
renderTrigger();
|
|
585
|
+
if (state.open) { search.hidden = !usesSearch(); renderList(); }
|
|
586
|
+
},
|
|
587
|
+
destroy() { close(false); removeListeners(); },
|
|
588
|
+
};
|
|
589
|
+
}
|
|
@@ -23,7 +23,8 @@ export function workCanvasNotes(work) {
|
|
|
23
23
|
const summary = work.summary;
|
|
24
24
|
if (summary?.state === 'complete' || summary?.state === 'partial') {
|
|
25
25
|
const needs = summary.needs_check?.length;
|
|
26
|
-
|
|
26
|
+
// `opens: 'city'`: the host lets the quest open the city details, where these records are listed.
|
|
27
|
+
if (needs > 0) notes.push({ label: '記録の確認', tone: 'attention', opens: 'city', summary: `記録の要確認 ${needs}件`, text: `読めた未完了の仕事のうち${needs}件に、記録から把握できていないことがあります。仕事が止まっているという意味ではありません。詳細の絞り込みから根拠を確認できます。` });
|
|
27
28
|
}
|
|
28
29
|
const at = work.reads?.tasks?.read_at;
|
|
29
30
|
notes.push({ label: '区画の時点', tone: 'info', text: `仕事の読み込み:${shortTime(at) ?? '時点不明'}。表示は読み込んだ時点の記録です。` });
|
|
@@ -170,15 +170,60 @@
|
|
|
170
170
|
font-size: 11px;
|
|
171
171
|
font-weight: 700;
|
|
172
172
|
}
|
|
173
|
-
.bb-world-canvas-ui .bb-world-canvas-quest-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
174
|
-
/* A quest with a way down (
|
|
175
|
-
|
|
173
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
174
|
+
/* A quest with a way down (候補の確認, 区画の入口) is pressable as a whole row even though the quest list lets
|
|
175
|
+
the map take the pointer: its one link or button stretches over the row. */
|
|
176
|
+
.bb-world-canvas-ui .bb-world-canvas-quest.has-way {
|
|
177
|
+
position: relative;
|
|
178
|
+
pointer-events: auto;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
transition: background-color 120ms ease, border-color 120ms ease;
|
|
181
|
+
}
|
|
182
|
+
.bb-world-canvas-ui .bb-world-canvas-quest.has-way:hover,
|
|
183
|
+
.bb-world-canvas-ui .bb-world-canvas-quest.has-way:focus-within {
|
|
184
|
+
border-color: rgb(155 232 145 / 55%);
|
|
185
|
+
background: linear-gradient(110deg, rgb(20 44 62 / 94%), rgb(20 44 62 / 84%));
|
|
186
|
+
}
|
|
187
|
+
.bb-world-canvas-ui .bb-world-canvas-quest a,
|
|
188
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-action {
|
|
176
189
|
flex: none;
|
|
190
|
+
margin: 0;
|
|
191
|
+
padding: 0;
|
|
192
|
+
border: 0;
|
|
193
|
+
background: none;
|
|
177
194
|
color: #b5f2ab;
|
|
195
|
+
font: inherit;
|
|
178
196
|
text-decoration: underline;
|
|
179
197
|
text-underline-offset: 2px;
|
|
198
|
+
cursor: pointer;
|
|
180
199
|
pointer-events: auto;
|
|
181
200
|
}
|
|
201
|
+
.bb-world-canvas-ui .bb-world-canvas-quest a::after,
|
|
202
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-action::after { content: " ›"; }
|
|
203
|
+
.bb-world-canvas-ui .bb-world-canvas-quest a::before,
|
|
204
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-action::before { content: ""; position: absolute; inset: 0; border-radius: 4px; }
|
|
205
|
+
.bb-world-canvas-ui .bb-world-canvas-quest a:focus-visible,
|
|
206
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-action:focus-visible { outline: none; }
|
|
207
|
+
.bb-world-canvas-ui .bb-world-canvas-quest a:focus-visible::before,
|
|
208
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-action:focus-visible::before { outline: 2px solid #9be891; outline-offset: 1px; }
|
|
209
|
+
/* A quest with nowhere to go: flat and dashed, so it reads as a sign, not a button, with its reason. */
|
|
210
|
+
.bb-world-canvas-ui .bb-world-canvas-quest.is-static {
|
|
211
|
+
border-style: dashed;
|
|
212
|
+
border-left-style: solid;
|
|
213
|
+
background: rgb(13 25 46 / 58%);
|
|
214
|
+
box-shadow: none;
|
|
215
|
+
}
|
|
216
|
+
/* On a narrow canvas the reason gives way before the row's own words. */
|
|
217
|
+
.bb-world-canvas-ui .bb-world-canvas-quest.is-static .bb-world-canvas-quest-text { flex: none; }
|
|
218
|
+
.bb-world-canvas-ui .bb-world-canvas-quest-reason {
|
|
219
|
+
flex: 0 1 auto;
|
|
220
|
+
min-width: 0;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
color: rgb(255 255 255 / 68%);
|
|
223
|
+
font-size: 11px;
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
text-overflow: ellipsis;
|
|
226
|
+
}
|
|
182
227
|
|
|
183
228
|
.bb-world-canvas-ui .bb-world-canvas-edge {
|
|
184
229
|
position: absolute;
|