@unson/brainbase-mcp 0.10.8 → 0.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/ui/project-workspace.css +164 -0
- package/ui/project-workspace.js +234 -169
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
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",
|
|
@@ -336,5 +336,5 @@
|
|
|
336
336
|
"engines": {
|
|
337
337
|
"node": ">=20"
|
|
338
338
|
},
|
|
339
|
-
"gitHead": "
|
|
339
|
+
"gitHead": "12af97cb3f02f9964c7bb7ca8ba7a5debdf29e11"
|
|
340
340
|
}
|
package/ui/project-workspace.css
CHANGED
|
@@ -218,6 +218,120 @@
|
|
|
218
218
|
flex-basis: 100%;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
.bb-pkw-context-task-list {
|
|
222
|
+
gap: 0;
|
|
223
|
+
border-top: 1px solid var(--bb-color-border);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.bb-pkw-context-list .bb-pkw-context-task-row {
|
|
227
|
+
display: grid;
|
|
228
|
+
grid-template-columns: 28px minmax(0, 2.4fr) minmax(112px, .75fr) 92px minmax(100px, .9fr);
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: 0;
|
|
231
|
+
min-height: 54px;
|
|
232
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
233
|
+
padding: 0;
|
|
234
|
+
font-size: 12px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.bb-pkw-context-list .bb-pkw-context-task-row:last-child {
|
|
238
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.bb-pkw-context-task-row > * {
|
|
242
|
+
min-width: 0;
|
|
243
|
+
padding: 8px 10px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.bb-pkw-context-task-row .bb-pkw-context-task-state {
|
|
247
|
+
width: 9px;
|
|
248
|
+
height: 9px;
|
|
249
|
+
margin: auto;
|
|
250
|
+
padding: 0;
|
|
251
|
+
border: 1.5px solid var(--bb-color-muted);
|
|
252
|
+
border-radius: 50%;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.bb-pkw-context-task-row .bb-pkw-context-task-state.bb-pkw-context-task-wait {
|
|
256
|
+
border-color: var(--bb-color-warning);
|
|
257
|
+
background: var(--bb-color-warning);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.bb-pkw-context-task-row .bb-pkw-context-task-state.bb-pkw-context-task-fail {
|
|
261
|
+
border-color: var(--bb-color-danger);
|
|
262
|
+
background: var(--bb-color-danger);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.bb-pkw-context-task-row .bb-pkw-context-task-state.bb-pkw-context-task-ok {
|
|
266
|
+
border-color: var(--bb-color-accent);
|
|
267
|
+
background: var(--bb-color-accent);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.bb-pkw-context-task-row .bb-pkw-context-task-title {
|
|
271
|
+
width: 100%;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
border: 0;
|
|
274
|
+
background: transparent;
|
|
275
|
+
color: var(--bb-color-ink);
|
|
276
|
+
font: inherit;
|
|
277
|
+
font-size: 14px;
|
|
278
|
+
font-weight: 690;
|
|
279
|
+
text-align: left;
|
|
280
|
+
text-overflow: ellipsis;
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
button.bb-pkw-context-task-title { cursor: pointer; }
|
|
285
|
+
button.bb-pkw-context-task-title:hover { color: var(--bb-color-accent-strong); text-decoration: underline; }
|
|
286
|
+
button.bb-pkw-context-task-title:focus-visible { outline: 3px solid var(--bb-color-focus); outline-offset: -2px; }
|
|
287
|
+
|
|
288
|
+
.bb-pkw-context-task-owner {
|
|
289
|
+
color: var(--bb-color-muted);
|
|
290
|
+
overflow-wrap: anywhere;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.bb-pkw-context-task-due {
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.bb-pkw-context-task-due.bb-pkw-context-task-overdue {
|
|
298
|
+
color: var(--bb-color-danger);
|
|
299
|
+
font-weight: 740;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.bb-pkw-context-task-stage {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
gap: 7px;
|
|
306
|
+
color: var(--bb-color-muted);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.bb-pkw-context-task-stage::before {
|
|
310
|
+
width: 6px;
|
|
311
|
+
height: 6px;
|
|
312
|
+
flex: 0 0 auto;
|
|
313
|
+
border-radius: 50%;
|
|
314
|
+
background: var(--bb-color-muted);
|
|
315
|
+
content: '';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.bb-pkw-context-task-stage.bb-pkw-context-task-wait::before { background: var(--bb-color-warning); }
|
|
319
|
+
.bb-pkw-context-task-stage.bb-pkw-context-task-fail::before { background: var(--bb-color-danger); }
|
|
320
|
+
.bb-pkw-context-task-stage.bb-pkw-context-task-ok::before { background: var(--bb-color-accent); }
|
|
321
|
+
|
|
322
|
+
@media (max-width: 760px) {
|
|
323
|
+
.bb-pkw-context-list .bb-pkw-context-task-row {
|
|
324
|
+
grid-template-columns: 28px minmax(0, 1fr) auto;
|
|
325
|
+
grid-template-areas: 'state title stage' '. owner due';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.bb-pkw-context-task-state { grid-area: state; }
|
|
329
|
+
.bb-pkw-context-task-title { grid-area: title; }
|
|
330
|
+
.bb-pkw-context-task-owner { grid-area: owner; }
|
|
331
|
+
.bb-pkw-context-task-due { grid-area: due; }
|
|
332
|
+
.bb-pkw-context-task-stage { grid-area: stage; }
|
|
333
|
+
}
|
|
334
|
+
|
|
221
335
|
.bb-pkw-info-grid {
|
|
222
336
|
display: grid;
|
|
223
337
|
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
|
@@ -819,6 +933,56 @@
|
|
|
819
933
|
gap: var(--bb-space-2);
|
|
820
934
|
}
|
|
821
935
|
|
|
936
|
+
.bb-pkw-context-directory {
|
|
937
|
+
margin-block: var(--bb-space-5);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.bb-pkw-directory-grid {
|
|
941
|
+
display: grid;
|
|
942
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
943
|
+
gap: var(--bb-space-3);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.bb-pkw-directory-card {
|
|
947
|
+
display: grid;
|
|
948
|
+
align-content: start;
|
|
949
|
+
gap: var(--bb-space-2);
|
|
950
|
+
min-width: 0;
|
|
951
|
+
border: 1px solid var(--bb-color-border);
|
|
952
|
+
border-radius: var(--bb-radius-sm);
|
|
953
|
+
padding: var(--bb-space-4);
|
|
954
|
+
background: var(--bb-color-panel);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.bb-pkw-directory-card h4,
|
|
958
|
+
.bb-pkw-directory-card p {
|
|
959
|
+
margin: 0;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.bb-pkw-directory-count {
|
|
963
|
+
font-size: 16px;
|
|
964
|
+
font-weight: 720;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.bb-pkw-directory-card .bb-pkw-dense-action {
|
|
968
|
+
justify-self: start;
|
|
969
|
+
margin-top: var(--bb-space-2);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.bb-pkw-list-pager {
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
justify-content: flex-end;
|
|
976
|
+
gap: var(--bb-space-3);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.bb-pkw-list-pager button:disabled {
|
|
980
|
+
color: var(--bb-color-muted);
|
|
981
|
+
cursor: default;
|
|
982
|
+
text-decoration: none;
|
|
983
|
+
opacity: .5;
|
|
984
|
+
}
|
|
985
|
+
|
|
822
986
|
.bb-pkw-work-search,
|
|
823
987
|
.bb-pkw-work-status,
|
|
824
988
|
.bb-pkw-work-owner,
|
package/ui/project-workspace.js
CHANGED
|
@@ -62,7 +62,7 @@ function normalizeContextItem(item) {
|
|
|
62
62
|
const title = cleanName(item.title, item.name);
|
|
63
63
|
if (!id || !title) return null;
|
|
64
64
|
const normalized = { id, title };
|
|
65
|
-
for (const key of ['kind', 'status', 'priority', 'summary', 'owner', 'assignee', 'dueAt', 'updatedAt', 'eventAt', 'body', 'description', 'sourceRecordId', 'relatedRecordId', 'relatedTaskId', 'sourcePath', 'provenance']) {
|
|
65
|
+
for (const key of ['kind', 'status', 'statusKind', 'priority', 'summary', 'owner', 'assignee', 'dueAt', 'updatedAt', 'eventAt', 'body', 'description', 'sourceRecordId', 'relatedRecordId', 'relatedTaskId', 'sourcePath', 'provenance']) {
|
|
66
66
|
const value = textOrNull(item[key]);
|
|
67
67
|
if (value) normalized[key] = value;
|
|
68
68
|
}
|
|
@@ -338,7 +338,46 @@ function contextSectionSkeleton(doc, { title = null, message = null } = {}) {
|
|
|
338
338
|
return group;
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
function
|
|
341
|
+
function contextTaskDue(value) {
|
|
342
|
+
if (!value) return '未確認';
|
|
343
|
+
const date = new Date(value);
|
|
344
|
+
return Number.isNaN(date.getTime())
|
|
345
|
+
? value
|
|
346
|
+
: new Intl.DateTimeFormat('ja-JP', { month: 'numeric', day: 'numeric' }).format(date);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function contextTaskOverdue(value, statusKind) {
|
|
350
|
+
if (!value || ['ok', 'fail'].includes(statusKind)) return false;
|
|
351
|
+
const date = new Date(value);
|
|
352
|
+
if (Number.isNaN(date.getTime())) return false;
|
|
353
|
+
const today = new Date();
|
|
354
|
+
date.setHours(0, 0, 0, 0);
|
|
355
|
+
today.setHours(0, 0, 0, 0);
|
|
356
|
+
return date < today;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function renderProjectContextTaskRow(doc, entry, onTaskOpen) {
|
|
360
|
+
const status = entry.status ?? '未確認';
|
|
361
|
+
const kind = ['ok', 'wait', 'fail'].includes(entry.statusKind) ? entry.statusKind : '';
|
|
362
|
+
const row = makeElement(doc, 'li', { className: 'bb-pkw-context-task-row' });
|
|
363
|
+
row.append(makeElement(doc, 'span', {
|
|
364
|
+
className: `bb-pkw-context-task-state${kind ? ` bb-pkw-context-task-${kind}` : ''}`,
|
|
365
|
+
attrs: { 'aria-hidden': 'true' },
|
|
366
|
+
}));
|
|
367
|
+
row.append(typeof onTaskOpen === 'function'
|
|
368
|
+
? denseButton(doc, entry.title, 'bb-pkw-context-task-title', () => onTaskOpen(entry.id), { title: entry.title, 'data-item-id': entry.id })
|
|
369
|
+
: makeElement(doc, 'strong', { className: 'bb-pkw-context-task-title', text: entry.title, attrs: { title: entry.title } }));
|
|
370
|
+
row.append(makeElement(doc, 'span', { className: 'bb-pkw-context-task-owner', text: entry.owner ?? entry.assignee ?? '担当者を確認できていない' }));
|
|
371
|
+
row.append(makeElement(doc, 'time', {
|
|
372
|
+
className: `bb-pkw-context-task-due${contextTaskOverdue(entry.dueAt, kind) ? ' bb-pkw-context-task-overdue' : ''}`,
|
|
373
|
+
text: contextTaskDue(entry.dueAt),
|
|
374
|
+
attrs: entry.dueAt ? { datetime: entry.dueAt } : {},
|
|
375
|
+
}));
|
|
376
|
+
row.append(makeElement(doc, 'span', { className: `bb-pkw-context-task-stage${kind ? ` bb-pkw-context-task-${kind}` : ''}`, text: status }));
|
|
377
|
+
return row;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function renderProjectContextGroup(doc, item, onTaskOpen) {
|
|
342
381
|
const group = makeElement(doc, 'section', {
|
|
343
382
|
className: `bb-pkw-context-group${item.state === 'loading' ? ' bb-pkw-context-group-loading' : ''}`,
|
|
344
383
|
attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
|
|
@@ -357,8 +396,13 @@ function renderProjectContextGroup(doc, item) {
|
|
|
357
396
|
} else if (item.state === 'unknown') {
|
|
358
397
|
group.append(workspaceNotice(doc, { label: '未確認', text: item.message ?? 'このセクションの取得状態は確認できません。', tone: 'warning' }));
|
|
359
398
|
} else if (item.items.length > 0) {
|
|
360
|
-
const
|
|
399
|
+
const taskList = item.items.every((entry) => entry.kind === 'task');
|
|
400
|
+
const list = makeElement(doc, 'ul', { className: `bb-pkw-context-list${taskList ? ' bb-pkw-context-task-list' : ''}` });
|
|
361
401
|
for (const entry of item.items) {
|
|
402
|
+
if (entry.kind === 'task') {
|
|
403
|
+
list.append(renderProjectContextTaskRow(doc, entry, onTaskOpen));
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
362
406
|
const row = makeElement(doc, 'li');
|
|
363
407
|
row.append(makeElement(doc, 'strong', { text: entry.title }));
|
|
364
408
|
if (entry.status) row.append(badge(doc, entry.status, 'muted'));
|
|
@@ -373,18 +417,12 @@ function renderProjectContextGroup(doc, item) {
|
|
|
373
417
|
return group;
|
|
374
418
|
}
|
|
375
419
|
|
|
376
|
-
function projectContextInsertionAnchor(doc) {
|
|
377
|
-
return makeElement(doc, 'span', {
|
|
378
|
-
className: 'bb-pkw-context-anchor',
|
|
379
|
-
attrs: { 'aria-hidden': 'true', hidden: 'true' },
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
420
|
/** Renders supplementary context while preserving each section's own state. */
|
|
384
421
|
export function renderProjectContext(doc, context, {
|
|
385
422
|
sections: selectedSections = null,
|
|
386
423
|
includeStatus = true,
|
|
387
424
|
includeHeading = true,
|
|
425
|
+
onTaskOpen = null,
|
|
388
426
|
} = {}) {
|
|
389
427
|
if (!context) return null;
|
|
390
428
|
const sections = Array.isArray(selectedSections)
|
|
@@ -407,7 +445,7 @@ export function renderProjectContext(doc, context, {
|
|
|
407
445
|
section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '補足情報のセクションはありません。' }));
|
|
408
446
|
return section;
|
|
409
447
|
}
|
|
410
|
-
for (const item of sections) section.append(renderProjectContextGroup(doc, item));
|
|
448
|
+
for (const item of sections) section.append(renderProjectContextGroup(doc, item, onTaskOpen));
|
|
411
449
|
return section;
|
|
412
450
|
}
|
|
413
451
|
|
|
@@ -417,12 +455,7 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
|
|
|
417
455
|
className: 'bb-pkw-panel bb-pkw-overview bb-pkw-overview-loading',
|
|
418
456
|
attrs: { 'aria-label': '概要' },
|
|
419
457
|
});
|
|
420
|
-
|
|
421
|
-
const readySections = contextSections.filter((item) => item?.state === 'ok' && Array.isArray(item.items) && item.items.length > 0);
|
|
422
|
-
const deferredSections = contextSections.filter((item) => !readySections.includes(item));
|
|
423
|
-
if (readySections.length > 0) {
|
|
424
|
-
panel.append(renderProjectContext(doc, context, { sections: readySections, includeStatus: false }));
|
|
425
|
-
}
|
|
458
|
+
panel.append(renderContextDirectory(doc, context));
|
|
426
459
|
const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
|
|
427
460
|
for (const title of ['目的と現在地', '関係者と役割']) {
|
|
428
461
|
const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
|
|
@@ -430,15 +463,6 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
|
|
|
430
463
|
columns.append(item);
|
|
431
464
|
}
|
|
432
465
|
panel.append(columns);
|
|
433
|
-
for (const title of ['記録された判断', '関連する情報']) {
|
|
434
|
-
const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
|
|
435
|
-
item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc, 2));
|
|
436
|
-
panel.append(item);
|
|
437
|
-
}
|
|
438
|
-
const contextBlock = readySections.length > 0
|
|
439
|
-
? (deferredSections.length > 0 ? renderProjectContext(doc, context, { sections: deferredSections, includeHeading: false }) : null)
|
|
440
|
-
: renderProjectContext(doc, context ?? { state: 'loading', sections: [] });
|
|
441
|
-
if (contextBlock) panel.append(contextBlock);
|
|
442
466
|
return panel;
|
|
443
467
|
}
|
|
444
468
|
|
|
@@ -475,6 +499,51 @@ function explicitSectionTotal(section) {
|
|
|
475
499
|
return null;
|
|
476
500
|
}
|
|
477
501
|
|
|
502
|
+
const CONTEXT_CATEGORIES = [
|
|
503
|
+
['tasks', 'タスク'], ['decisions', '判断'], ['knowledge', '知識'], ['glossary', '用語集'],
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
function contextSectionState(context, section) {
|
|
507
|
+
if (section) return section.state;
|
|
508
|
+
if (context?.state === 'failed') return 'failed';
|
|
509
|
+
if (context?.state === 'loading') return 'loading';
|
|
510
|
+
if (context?.state === 'unknown') return 'unknown';
|
|
511
|
+
return 'unavailable';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function contextCountLabel(context, section) {
|
|
515
|
+
if (contextSectionState(context, section) !== 'ok') return '件数未確認';
|
|
516
|
+
const total = explicitSectionTotal(section);
|
|
517
|
+
if (total === null) return `${section.items.length}件取得済み · 総数未確認`;
|
|
518
|
+
return `${total}件 · ${section.items.length}件取得済み${total > section.items.length ? ' · 続き未取得' : ''}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderContextDirectory(doc, context, actions = {}) {
|
|
522
|
+
const panel = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-context-directory', attrs: { 'aria-label': '項目別の一覧' } });
|
|
523
|
+
panel.append(makeElement(doc, 'h3', { text: '項目別に探す' }));
|
|
524
|
+
if (context?.state && context.state !== 'ok') panel.append(workspaceNotice(doc, {
|
|
525
|
+
label: context.state === 'loading' ? '読み込み中' : context.state === 'failed' ? '読み取り失敗' : '未確認',
|
|
526
|
+
text: context.message ?? '項目の取得状態を確認できません。',
|
|
527
|
+
tone: context.state === 'failed' ? 'danger' : 'info',
|
|
528
|
+
}));
|
|
529
|
+
const grid = makeElement(doc, 'div', { className: 'bb-pkw-directory-grid' });
|
|
530
|
+
for (const [id, title] of CONTEXT_CATEGORIES) {
|
|
531
|
+
const section = contextSectionById(context, [id]);
|
|
532
|
+
const state = contextSectionState(context, section);
|
|
533
|
+
const card = makeElement(doc, 'div', { className: 'bb-pkw-directory-card' });
|
|
534
|
+
card.append(makeElement(doc, 'h4', { text: title }));
|
|
535
|
+
card.append(makeElement(doc, 'p', { className: 'bb-pkw-directory-count', text: contextCountLabel(context, section) }));
|
|
536
|
+
const stateLabel = state === 'ok'
|
|
537
|
+
? (explicitSectionTotal(section) === 0 && section.items.length === 0 ? '該当項目なし' : '取得済み')
|
|
538
|
+
: state === 'loading' ? '読み込み中' : state === 'failed' ? '読み取り失敗' : state === 'unavailable' ? '未接続' : '未確認';
|
|
539
|
+
card.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: stateLabel }));
|
|
540
|
+
if (typeof actions.openSectionList === 'function') card.append(denseButton(doc, '一覧を見る', 'bb-pkw-dense-action', () => actions.openSectionList(id), { 'aria-label': `${title}の一覧を見る` }));
|
|
541
|
+
grid.append(card);
|
|
542
|
+
}
|
|
543
|
+
panel.append(grid);
|
|
544
|
+
return panel;
|
|
545
|
+
}
|
|
546
|
+
|
|
478
547
|
function denseDate(value) {
|
|
479
548
|
const text = textOrNull(value);
|
|
480
549
|
if (!text) return '未確認';
|
|
@@ -524,14 +593,14 @@ function denseContextStateNotice(doc, section, fallback = 'この情報はまだ
|
|
|
524
593
|
return workspaceNotice(doc, { label, text: section.message ?? fallback, tone, role: section.state === 'failed' ? 'alert' : 'status' });
|
|
525
594
|
}
|
|
526
595
|
|
|
527
|
-
function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false } = {}) {
|
|
596
|
+
function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false, includeSummary = true } = {}) {
|
|
528
597
|
const row = makeElement(doc, 'li', { className: `bb-pkw-dense-item bb-pkw-dense-item-${kind}` });
|
|
529
598
|
const head = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-head' });
|
|
530
599
|
const title = cleanName(item.title, item.name) ?? '名前不明';
|
|
531
600
|
if (typeof onOpen === 'function' && item.id) head.append(denseButton(doc, title, 'bb-pkw-dense-link', () => onOpen(item.id), { 'data-item-id': item.id }));
|
|
532
601
|
else head.append(makeElement(doc, 'strong', { text: title }));
|
|
533
602
|
if (item.status) head.append(badge(doc, item.status, 'muted'));
|
|
534
|
-
row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table }));
|
|
603
|
+
row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table && includeSummary }));
|
|
535
604
|
return row;
|
|
536
605
|
}
|
|
537
606
|
|
|
@@ -566,10 +635,10 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
|
|
|
566
635
|
const tasks = contextTaskItems(context);
|
|
567
636
|
const records = contextRecordItems(context);
|
|
568
637
|
const graphRecords = [...(entities?.values?.() ?? [])].filter((entity) => !['project', 'person'].includes(entity.type));
|
|
569
|
-
const
|
|
570
|
-
.
|
|
571
|
-
.
|
|
572
|
-
|
|
638
|
+
const participantEdges = [...new Map((payload?.participants ?? [])
|
|
639
|
+
.filter((edge) => edge?.counterpart?.id && (edge.counterpart.name || edge.counterpart.title))
|
|
640
|
+
.map((edge) => [edge.counterpart.id, edge])).values()];
|
|
641
|
+
const participants = participantEdges.slice(0, 6);
|
|
573
642
|
const recentRecords = records.length > 0 ? sortDenseItems(records).slice(0, 4) : graphRecords.slice(0, 4);
|
|
574
643
|
const updated = [...tasks, ...records]
|
|
575
644
|
.filter((item) => item.updatedAt || item.eventAt)
|
|
@@ -588,7 +657,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
|
|
|
588
657
|
denseMetric(doc, '仕事', taskMetric, taskNote),
|
|
589
658
|
denseMetric(doc, '今日扱う仕事', taskSection?.state === 'ok' ? `${due.length}件` : '未確認', taskSection?.state === 'ok' ? '期限・更新が確認できる順' : 'タスクの状態を確認中'),
|
|
590
659
|
denseMetric(doc, '前回からの変化', updated.length > 0 ? `${updated.length}件` : '未確認', updated.length > 0 ? '更新時点がある記録' : '更新時点が未確認'),
|
|
591
|
-
denseMetric(doc, '関係者',
|
|
660
|
+
denseMetric(doc, '関係者', participantEdges.length > 0 ? `${participantEdges.length}人` : '未確認', 'Graphに記録された参加関係'),
|
|
592
661
|
);
|
|
593
662
|
panel.append(metrics);
|
|
594
663
|
|
|
@@ -643,10 +712,13 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
|
|
|
643
712
|
if (participants.length === 0) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'Graphで確認できる関係者は未確認です。' }));
|
|
644
713
|
else {
|
|
645
714
|
const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
|
|
646
|
-
for (const
|
|
715
|
+
for (const edge of participants) {
|
|
716
|
+
const person = edge.counterpart;
|
|
647
717
|
const row = makeElement(doc, 'li', { className: 'bb-pkw-dense-item' });
|
|
648
|
-
row.append(
|
|
649
|
-
|
|
718
|
+
row.append(recordLink(doc, cleanName(person.name, person.title), person.id, actions.openGraphEntity, 'bb-pkw-dense-link'));
|
|
719
|
+
row.append(badge(doc, relationLabel(edge.relation), edge.relation === 'accountable_for' ? 'accent' : 'muted'));
|
|
720
|
+
const facts = [textOrNull(edge.role) ? `役割: ${edge.role}` : null, textOrNull(edge.context) ? `文脈: ${edge.context}` : null].filter(Boolean);
|
|
721
|
+
if (facts.length > 0) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
|
|
650
722
|
list.append(row);
|
|
651
723
|
}
|
|
652
724
|
people.append(list);
|
|
@@ -656,7 +728,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
|
|
|
656
728
|
return panel;
|
|
657
729
|
}
|
|
658
730
|
|
|
659
|
-
function renderWorkList(doc, context, actions = {}) {
|
|
731
|
+
function renderWorkList(doc, context, actions = {}, state = {}) {
|
|
660
732
|
const section = contextTaskSection(context);
|
|
661
733
|
const allItems = contextTaskItems(context);
|
|
662
734
|
const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-work-list', attrs: { role: 'tabpanel', 'aria-label': '仕事一覧' } });
|
|
@@ -681,10 +753,19 @@ function renderWorkList(doc, context, actions = {}) {
|
|
|
681
753
|
makeElement(doc, 'option', { text: '期限未確認', attrs: { value: 'missing' } }),
|
|
682
754
|
);
|
|
683
755
|
controls.append(search, status, owner, due);
|
|
756
|
+
search.value = state.query ?? '';
|
|
757
|
+
status.value = state.status ?? '';
|
|
758
|
+
owner.value = state.owner ?? '';
|
|
759
|
+
due.value = state.due ?? '';
|
|
684
760
|
panel.append(controls);
|
|
685
761
|
const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
|
|
686
762
|
const rows = makeElement(doc, 'div', { className: 'bb-pkw-work-rows', attrs: { role: 'list', 'aria-label': '仕事の行' } });
|
|
687
|
-
|
|
763
|
+
const pager = makeElement(doc, 'nav', { className: 'bb-pkw-list-pager', attrs: { 'aria-label': '仕事一覧のページ' } });
|
|
764
|
+
const previous = denseButton(doc, '前へ', 'bb-pkw-dense-action', () => { state.page -= 1; renderRows(); });
|
|
765
|
+
const next = denseButton(doc, '次へ', 'bb-pkw-dense-action', () => { state.page += 1; renderRows(); });
|
|
766
|
+
const pageLabel = makeElement(doc, 'span', { className: 'bb-pkw-muted' });
|
|
767
|
+
pager.append(previous, pageLabel, next);
|
|
768
|
+
panel.append(count, rows, pager);
|
|
688
769
|
const renderRows = () => {
|
|
689
770
|
const query = String(search.value ?? '').trim().toLocaleLowerCase();
|
|
690
771
|
const selectedStatus = String(status.value ?? '');
|
|
@@ -699,8 +780,14 @@ function renderWorkList(doc, context, actions = {}) {
|
|
|
699
780
|
return [item.title, item.summary, item.owner, item.assignee, item.status].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
|
|
700
781
|
});
|
|
701
782
|
const total = explicitSectionTotal(section);
|
|
702
|
-
|
|
703
|
-
|
|
783
|
+
state.page = Math.max(0, Math.min(state.page ?? 0, Math.max(0, Math.ceil(visible.length / 16) - 1)));
|
|
784
|
+
const display = visible.slice(state.page * 16, (state.page + 1) * 16);
|
|
785
|
+
const start = display.length > 0 ? state.page * 16 + 1 : 0;
|
|
786
|
+
const end = state.page * 16 + display.length;
|
|
787
|
+
count.textContent = `取得済み ${allItems.length}件 · 条件一致 ${visible.length}件 · 表示 ${start}–${end}件 · 総数 ${section?.state === 'ok' && total !== null ? `${total}件${total > allItems.length ? '(続き未取得)' : ''}` : '未確認'}`;
|
|
788
|
+
pageLabel.textContent = `${state.page + 1}ページ目`;
|
|
789
|
+
previous.disabled = state.page === 0;
|
|
790
|
+
next.disabled = (state.page + 1) * 16 >= visible.length;
|
|
704
791
|
rows.replaceChildren();
|
|
705
792
|
if (display.length === 0) {
|
|
706
793
|
rows.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する仕事はありません。' : '仕事の行は未確認です。' }));
|
|
@@ -715,10 +802,71 @@ function renderWorkList(doc, context, actions = {}) {
|
|
|
715
802
|
rows.append(row);
|
|
716
803
|
}
|
|
717
804
|
};
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
805
|
+
const resetRows = () => {
|
|
806
|
+
Object.assign(state, { query: search.value, status: status.value, owner: owner.value, due: due.value, page: 0 });
|
|
807
|
+
renderRows();
|
|
808
|
+
};
|
|
809
|
+
search.addEventListener('input', resetRows);
|
|
810
|
+
status.addEventListener('change', resetRows);
|
|
811
|
+
owner.addEventListener('change', resetRows);
|
|
812
|
+
due.addEventListener('change', resetRows);
|
|
813
|
+
renderRows();
|
|
814
|
+
return panel;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function renderRecordList(doc, context, sectionId, actions = {}, state = {}) {
|
|
818
|
+
const section = contextSectionById(context, [sectionId]);
|
|
819
|
+
const title = CONTEXT_CATEGORIES.find(([id]) => id === sectionId)?.[1] ?? section?.title ?? '記録';
|
|
820
|
+
const items = Array.isArray(section?.items) ? section.items : [];
|
|
821
|
+
const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-record-list', attrs: { role: 'tabpanel', 'aria-label': `${title}一覧` } });
|
|
822
|
+
const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
|
|
823
|
+
heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'h3', { text: `${title}一覧` }));
|
|
824
|
+
panel.append(heading);
|
|
825
|
+
const notice = denseContextStateNotice(doc, section ?? (context?.state !== 'ok' ? context : { state: 'unknown' }), 'この区分を読み取れませんでした。');
|
|
826
|
+
if (notice) panel.append(notice);
|
|
827
|
+
const controls = makeElement(doc, 'div', { className: 'bb-pkw-work-controls', attrs: { 'aria-label': `${title}の絞り込み` } });
|
|
828
|
+
const search = makeElement(doc, 'input', { className: 'bb-pkw-work-search', attrs: { type: 'search', name: 'record-search', placeholder: `${title}を検索`, 'aria-label': `${title}を検索` } });
|
|
829
|
+
const status = makeElement(doc, 'select', { className: 'bb-pkw-work-status', attrs: { name: 'record-status', 'aria-label': '状態で絞り込む' } });
|
|
830
|
+
status.append(makeElement(doc, 'option', { text: 'すべての状態', attrs: { value: '' } }));
|
|
831
|
+
for (const value of new Set(items.map((item) => item.status).filter(Boolean))) status.append(makeElement(doc, 'option', { text: value, attrs: { value } }));
|
|
832
|
+
controls.append(search, status);
|
|
833
|
+
search.value = state.query ?? '';
|
|
834
|
+
status.value = state.status ?? '';
|
|
835
|
+
panel.append(controls);
|
|
836
|
+
const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
|
|
837
|
+
const rows = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list', attrs: { 'aria-label': `${title}の行` } });
|
|
838
|
+
const pager = makeElement(doc, 'nav', { className: 'bb-pkw-list-pager', attrs: { 'aria-label': `${title}一覧のページ` } });
|
|
839
|
+
const previous = denseButton(doc, '前へ', 'bb-pkw-dense-action', () => { state.page -= 1; renderRows(); });
|
|
840
|
+
const next = denseButton(doc, '次へ', 'bb-pkw-dense-action', () => { state.page += 1; renderRows(); });
|
|
841
|
+
const pageLabel = makeElement(doc, 'span', { className: 'bb-pkw-muted' });
|
|
842
|
+
pager.append(previous, pageLabel, next);
|
|
843
|
+
panel.append(count, rows, pager);
|
|
844
|
+
const renderRows = () => {
|
|
845
|
+
const query = String(search.value ?? '').trim().toLocaleLowerCase();
|
|
846
|
+
const selectedStatus = String(status.value ?? '');
|
|
847
|
+
const visible = items.filter((item) => {
|
|
848
|
+
if (selectedStatus && item.status !== selectedStatus) return false;
|
|
849
|
+
return !query || [item.title, item.summary, item.status, item.owner, item.body].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
|
|
850
|
+
});
|
|
851
|
+
state.page = Math.max(0, Math.min(state.page ?? 0, Math.max(0, Math.ceil(visible.length / 16) - 1)));
|
|
852
|
+
const display = visible.slice(state.page * 16, (state.page + 1) * 16);
|
|
853
|
+
const start = display.length > 0 ? state.page * 16 + 1 : 0;
|
|
854
|
+
const end = state.page * 16 + display.length;
|
|
855
|
+
const total = explicitSectionTotal(section);
|
|
856
|
+
count.textContent = `取得済み ${items.length}件 · 条件一致 ${visible.length}件 · 表示 ${start}–${end}件 · 総数 ${section?.state === 'ok' && total !== null ? `${total}件${total > items.length ? '(続き未取得)' : ''}` : '未確認'}`;
|
|
857
|
+
pageLabel.textContent = `${state.page + 1}ページ目`;
|
|
858
|
+
previous.disabled = state.page === 0;
|
|
859
|
+
next.disabled = (state.page + 1) * 16 >= visible.length;
|
|
860
|
+
rows.replaceChildren();
|
|
861
|
+
if (visible.length === 0) {
|
|
862
|
+
rows.append(makeElement(doc, 'li', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する記録はありません。' : '記録の行は未確認です。' }));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
for (const item of display) rows.append(renderDenseItemRow(doc, item, { kind: 'record', onOpen: actions.openRecordDetail, includeSummary: false }));
|
|
866
|
+
};
|
|
867
|
+
const resetRows = () => { Object.assign(state, { query: search.value, status: status.value, page: 0 }); renderRows(); };
|
|
868
|
+
search.addEventListener('input', resetRows);
|
|
869
|
+
status.addEventListener('change', resetRows);
|
|
722
870
|
renderRows();
|
|
723
871
|
return panel;
|
|
724
872
|
}
|
|
@@ -779,7 +927,7 @@ function renderWorkDetail(doc, item, context, actions = {}) {
|
|
|
779
927
|
function renderRecordDetail(doc, record, context, actions = {}) {
|
|
780
928
|
const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '記録詳細' } });
|
|
781
929
|
const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
|
|
782
|
-
heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
|
|
930
|
+
heading.append(denseButton(doc, actions.recordSectionTitle ? `← ${actions.recordSectionTitle}一覧` : '← 概要', 'bb-pkw-back', actions.recordSectionTitle ? () => actions.openSectionList?.(actions.recordSectionId) : actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
|
|
783
931
|
heading.children[1].append(makeElement(doc, 'h3', { text: cleanName(record?.title, record?.name) ?? '記録詳細' }), record?.kind || record?.type ? badge(doc, typeLabel(record.kind ?? record.type), 'accent') : null);
|
|
784
932
|
panel.append(heading);
|
|
785
933
|
if (!record) {
|
|
@@ -814,12 +962,12 @@ function renderRecordDetail(doc, record, context, actions = {}) {
|
|
|
814
962
|
return panel;
|
|
815
963
|
}
|
|
816
964
|
|
|
817
|
-
function renderOverview(doc, detail, context,
|
|
965
|
+
function renderOverview(doc, detail, context, entities, actions = {}) {
|
|
818
966
|
const payload = projectDetailPayload(detail);
|
|
819
967
|
const project = payload?.project;
|
|
820
968
|
const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
|
|
821
969
|
panel.append(renderDenseOverview(doc, detail, context, entities, actions));
|
|
822
|
-
|
|
970
|
+
panel.append(renderContextDirectory(doc, context, actions));
|
|
823
971
|
const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
|
|
824
972
|
summary.append(makeElement(doc, 'h3', { text: '目的と現在地' }), workspaceDefinition(doc, [
|
|
825
973
|
['目的', textOrNull(project?.goal)],
|
|
@@ -828,83 +976,7 @@ function renderOverview(doc, detail, context, edges, entities, onSelect, onConte
|
|
|
828
976
|
['出典', sourceLabel(payload?.source)],
|
|
829
977
|
['読み取った時点', payload?.asOf ? formatDateTime(payload.asOf) : null],
|
|
830
978
|
]));
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const people = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '関係者' } });
|
|
834
|
-
people.append(makeElement(doc, 'h3', { text: '関係者と役割' }));
|
|
835
|
-
const participantEdges = Array.isArray(payload?.participants) ? payload.participants : [];
|
|
836
|
-
if (participantEdges.length === 0) {
|
|
837
|
-
people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関係者の記録はありません。' }));
|
|
838
|
-
} else {
|
|
839
|
-
const list = makeElement(doc, 'ul', { className: 'bb-pkw-people-list' });
|
|
840
|
-
for (const edge of participantEdges) {
|
|
841
|
-
const item = makeElement(doc, 'li');
|
|
842
|
-
const counterpart = edge.counterpart ?? {};
|
|
843
|
-
item.append(recordLink(doc, cleanName(counterpart.name, counterpart.id) ?? '名前不明', cleanId(counterpart.id), onSelect), badge(doc, relationLabel(edge.relation), edge.relation === 'accountable_for' ? 'accent' : 'muted'));
|
|
844
|
-
const facts = [textOrNull(edge.role) ? `役割: ${edge.role}` : null, textOrNull(edge.context) ? `文脈: ${edge.context}` : null].filter(Boolean);
|
|
845
|
-
if (facts.length > 0) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
|
|
846
|
-
list.append(item);
|
|
847
|
-
}
|
|
848
|
-
people.append(list);
|
|
849
|
-
}
|
|
850
|
-
columns.append(people);
|
|
851
|
-
panel.append(columns);
|
|
852
|
-
|
|
853
|
-
const contextBlock = renderProjectContext(doc, context);
|
|
854
|
-
if (contextBlock) {
|
|
855
|
-
panel.append(contextBlock);
|
|
856
|
-
if (typeof onContext === 'function') onContext(contextBlock, null);
|
|
857
|
-
} else {
|
|
858
|
-
const contextAnchor = projectContextInsertionAnchor(doc);
|
|
859
|
-
panel.append(contextAnchor);
|
|
860
|
-
if (typeof onContext === 'function') onContext(null, contextAnchor);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const decisions = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '記録された判断' } });
|
|
864
|
-
decisions.append(makeElement(doc, 'h3', { text: '記録された判断' }));
|
|
865
|
-
const decisionEdges = edges.filter((edge) => entities.get(edge.source)?.type === 'decision' || entities.get(edge.target)?.type === 'decision');
|
|
866
|
-
if (decisionEdges.length === 0) decisions.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'このプロジェクトに結びつく判断の記録はありません。' }));
|
|
867
|
-
else {
|
|
868
|
-
const list = makeElement(doc, 'ul', { className: 'bb-pkw-decision-list' });
|
|
869
|
-
for (const edge of decisionEdges) {
|
|
870
|
-
const decisionId = entities.get(edge.source)?.type === 'decision' ? edge.source : edge.target;
|
|
871
|
-
const decision = entities.get(decisionId);
|
|
872
|
-
const item = makeElement(doc, 'li');
|
|
873
|
-
item.append(recordLink(doc, decision?.name ?? decisionId, decisionId, onSelect), badge(doc, relationLabel(edge.relation), 'muted'));
|
|
874
|
-
if (decision?.summary) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: decision.summary }));
|
|
875
|
-
list.append(item);
|
|
876
|
-
}
|
|
877
|
-
decisions.append(list);
|
|
878
|
-
}
|
|
879
|
-
panel.append(decisions);
|
|
880
|
-
|
|
881
|
-
const objects = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '関連する情報' } });
|
|
882
|
-
objects.append(makeElement(doc, 'h3', { text: '関連する情報' }));
|
|
883
|
-
const groups = new Map();
|
|
884
|
-
for (const entity of entities.values()) {
|
|
885
|
-
if (entity.id === project?.id || entity.type === 'person' || entity.type === 'decision') continue;
|
|
886
|
-
if (!groups.has(entity.type)) groups.set(entity.type, []);
|
|
887
|
-
groups.get(entity.type).push(entity);
|
|
888
|
-
}
|
|
889
|
-
if (groups.size === 0) objects.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連する情報の記録はありません。' }));
|
|
890
|
-
else {
|
|
891
|
-
const grid = makeElement(doc, 'div', { className: 'bb-pkw-info-grid' });
|
|
892
|
-
for (const [type, records] of groups) {
|
|
893
|
-
const group = makeElement(doc, 'section', { className: 'bb-pkw-object', attrs: { 'aria-label': typeLabel(type) } });
|
|
894
|
-
group.append(makeElement(doc, 'h4', { text: typeLabel(type) }));
|
|
895
|
-
const list = makeElement(doc, 'ul');
|
|
896
|
-
for (const record of records) {
|
|
897
|
-
const item = makeElement(doc, 'li');
|
|
898
|
-
item.append(recordLink(doc, record.name, record.id, onSelect));
|
|
899
|
-
list.append(item);
|
|
900
|
-
}
|
|
901
|
-
group.append(list);
|
|
902
|
-
grid.append(group);
|
|
903
|
-
}
|
|
904
|
-
objects.append(grid);
|
|
905
|
-
}
|
|
906
|
-
panel.append(objects);
|
|
907
|
-
|
|
979
|
+
panel.append(summary);
|
|
908
980
|
const unknowns = projectKnowledgeUnknowns(detail);
|
|
909
981
|
if (unknowns.length > 0) {
|
|
910
982
|
const missing = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-unknown', attrs: { 'aria-label': '未記録・未確認' } });
|
|
@@ -955,13 +1027,16 @@ export function createProjectKnowledgeWorkspace({
|
|
|
955
1027
|
let currentContext = context;
|
|
956
1028
|
let activeView = 'overview';
|
|
957
1029
|
let selectedContextId = null;
|
|
1030
|
+
let selectedSectionId = null;
|
|
1031
|
+
const listViewStates = new Map();
|
|
1032
|
+
const listViewState = (id) => {
|
|
1033
|
+
if (!listViewStates.has(id)) listViewStates.set(id, {});
|
|
1034
|
+
return listViewStates.get(id);
|
|
1035
|
+
};
|
|
958
1036
|
// Keep direct references as well as the DOM lookup. The local test harness
|
|
959
1037
|
// intentionally has no querySelector implementation, and a host can replace
|
|
960
1038
|
// the panel while a detail read is still in flight.
|
|
961
1039
|
let selectedPanel = null;
|
|
962
|
-
let overviewPanel = null;
|
|
963
|
-
let contextBlock = null;
|
|
964
|
-
let contextAnchor = null;
|
|
965
1040
|
|
|
966
1041
|
const root = makeElement(doc, 'section', {
|
|
967
1042
|
className: 'bb-pkw',
|
|
@@ -1007,6 +1082,7 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1007
1082
|
activeTab = 'overview';
|
|
1008
1083
|
activeView = 'overview';
|
|
1009
1084
|
selectedContextId = null;
|
|
1085
|
+
selectedSectionId = null;
|
|
1010
1086
|
render();
|
|
1011
1087
|
}
|
|
1012
1088
|
|
|
@@ -1015,6 +1091,17 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1015
1091
|
activeTab = 'overview';
|
|
1016
1092
|
activeView = 'work-list';
|
|
1017
1093
|
selectedContextId = null;
|
|
1094
|
+
selectedSectionId = 'tasks';
|
|
1095
|
+
render();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function openSectionList(id) {
|
|
1099
|
+
if (destroyed) return;
|
|
1100
|
+
if (id === 'tasks') return openWorkList();
|
|
1101
|
+
activeTab = 'overview';
|
|
1102
|
+
activeView = 'record-list';
|
|
1103
|
+
selectedSectionId = id;
|
|
1104
|
+
selectedContextId = null;
|
|
1018
1105
|
render();
|
|
1019
1106
|
}
|
|
1020
1107
|
|
|
@@ -1031,6 +1118,8 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1031
1118
|
activeTab = 'overview';
|
|
1032
1119
|
activeView = 'record-detail';
|
|
1033
1120
|
selectedContextId = cleanId(id);
|
|
1121
|
+
const sourceSection = currentContext?.sections?.find((section) => section.id !== contextTaskSection(currentContext)?.id && section.items?.some((item) => item.id === selectedContextId));
|
|
1122
|
+
selectedSectionId = sourceSection?.id ?? null;
|
|
1034
1123
|
render();
|
|
1035
1124
|
}
|
|
1036
1125
|
|
|
@@ -1245,39 +1334,17 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1245
1334
|
|
|
1246
1335
|
function replaceContextBlock(nextContext) {
|
|
1247
1336
|
currentContext = nextContext;
|
|
1248
|
-
if (destroyed) return;
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
contextAnchor = nextAnchor;
|
|
1260
|
-
} else if (contextBlock?.parentNode?.removeChild) {
|
|
1261
|
-
contextBlock.parentNode.removeChild(contextBlock);
|
|
1262
|
-
contextBlock = null;
|
|
1263
|
-
}
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
|
|
1267
|
-
contextBlock.parentNode.replaceChild(nextBlock, contextBlock);
|
|
1268
|
-
contextBlock = nextBlock;
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
if (contextAnchor?.parentNode && typeof contextAnchor.parentNode.replaceChild === 'function') {
|
|
1272
|
-
contextAnchor.parentNode.replaceChild(nextBlock, contextAnchor);
|
|
1273
|
-
contextBlock = nextBlock;
|
|
1274
|
-
contextAnchor = null;
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
if (contextBlock?.replaceWith) {
|
|
1278
|
-
contextBlock.replaceWith(nextBlock);
|
|
1279
|
-
contextBlock = nextBlock;
|
|
1280
|
-
return;
|
|
1337
|
+
if (destroyed || activeTab !== 'overview') return;
|
|
1338
|
+
const focused = doc.activeElement;
|
|
1339
|
+
const controlName = root.contains?.(focused) && focused?.closest?.('.bb-pkw-work-controls') ? focused.getAttribute?.('name') : null;
|
|
1340
|
+
const restoreFocus = ['work-search', 'work-status', 'work-owner', 'work-due', 'record-search', 'record-status'].includes(controlName);
|
|
1341
|
+
const selectionStart = restoreFocus && focused?.tagName === 'INPUT' ? focused.selectionStart : null;
|
|
1342
|
+
const selectionEnd = restoreFocus && focused?.tagName === 'INPUT' ? focused.selectionEnd : null;
|
|
1343
|
+
render();
|
|
1344
|
+
if (restoreFocus) {
|
|
1345
|
+
const replacement = root.querySelector?.(`[name="${controlName}"]`);
|
|
1346
|
+
replacement?.focus?.();
|
|
1347
|
+
if (selectionStart !== null && selectionEnd !== null && typeof replacement?.setSelectionRange === 'function') replacement.setSelectionRange(selectionStart, selectionEnd);
|
|
1281
1348
|
}
|
|
1282
1349
|
}
|
|
1283
1350
|
|
|
@@ -1287,20 +1354,18 @@ export function createProjectKnowledgeWorkspace({
|
|
|
1287
1354
|
teardownGraph();
|
|
1288
1355
|
tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
|
|
1289
1356
|
panels.replaceChildren();
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1357
|
+
const actions = { openOverview, openWorkList, openWorkDetail, openRecordDetail, openSectionList, openGraphEntity: selectAndExplore,
|
|
1358
|
+
recordSectionId: selectedSectionId,
|
|
1359
|
+
recordSectionTitle: CONTEXT_CATEGORIES.find(([id]) => id === selectedSectionId)?.[1] ?? contextSectionById(currentContext, [selectedSectionId])?.title,
|
|
1360
|
+
};
|
|
1293
1361
|
let overview;
|
|
1294
|
-
if (activeView === 'work-list') overview = renderWorkList(doc, currentContext, actions);
|
|
1362
|
+
if (activeView === 'work-list') overview = renderWorkList(doc, currentContext, actions, listViewState('tasks'));
|
|
1363
|
+
else if (activeView === 'record-list') overview = renderRecordList(doc, currentContext, selectedSectionId, actions, listViewState(selectedSectionId));
|
|
1295
1364
|
else if (activeView === 'work-detail') overview = renderWorkDetail(doc, findContextItem(selectedContextId, 'task'), currentContext, actions);
|
|
1296
1365
|
else if (activeView === 'record-detail') overview = renderRecordDetail(doc, findContextItem(selectedContextId, 'record') ?? entities.get(selectedContextId), currentContext, actions);
|
|
1297
|
-
else overview = renderOverview(doc, payload, currentContext,
|
|
1298
|
-
contextBlock = node;
|
|
1299
|
-
contextAnchor = anchor;
|
|
1300
|
-
}, actions);
|
|
1366
|
+
else overview = renderOverview(doc, payload, currentContext, overviewEntities, actions);
|
|
1301
1367
|
overview.id = 'bb-pkw-panel-overview';
|
|
1302
1368
|
overview.hidden = activeTab !== 'overview';
|
|
1303
|
-
overviewPanel = overview;
|
|
1304
1369
|
panels.append(overview);
|
|
1305
1370
|
const graph = renderGraphPanel();
|
|
1306
1371
|
graph.hidden = activeTab !== 'graph';
|