@unson/brainbase-mcp 0.37.0 → 0.39.0

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.
@@ -56,6 +56,7 @@ export const GRAPH_RELATION_LABELS = Object.freeze({
56
56
  participates_in: Object.freeze({ label: '参加', meaning: '人物がプロジェクトに参加している', outgoing: 'に参加', incoming: 'が参加' }),
57
57
  accountable_for: Object.freeze({ label: '責任', meaning: '人物がプロジェクトの成果や判断に責任を持つ', outgoing: 'に責任を持つ', incoming: 'が責任を持つ' }),
58
58
  owned_by: Object.freeze({ label: '所有', meaning: 'プロジェクトを組織が所有している', outgoing: 'が所有', incoming: 'を所有' }),
59
+ related_to: Object.freeze({ label: '関わる', meaning: '会社が顧客・協業先などとしてプロジェクトに関わっている', outgoing: 'に関わる', incoming: 'が関わる' }),
59
60
  governs: Object.freeze({ label: '方針', meaning: '判断や原則がプロジェクトの進め方を決める', outgoing: 'の進め方を決める', incoming: 'が進め方を決める' }),
60
61
  supersedes: Object.freeze({ label: '置き換え', meaning: '判断が以前の判断を置き換える', outgoing: 'を置き換える', incoming: 'に置き換えられた' }),
61
62
  });
@@ -101,6 +102,7 @@ const CORRECTION_KIND_LABELS = Object.freeze({
101
102
  update_entity: '記録を直した',
102
103
  update_edge: '関係を直した',
103
104
  create_edge: '関係を加えた',
105
+ create_entity: '記録を加えた',
104
106
  });
105
107
 
106
108
  /** Inputs of the correction form. `date` inputs hold a local day (YYYY-MM-DD). */
@@ -127,6 +129,9 @@ export const GRAPH_CORRECTION_ERROR_MESSAGES = Object.freeze({
127
129
  correction_relation_change_not_allowed: '関係の種類は変えられません。今の関係を終えてから、新しい関係を加えてください。',
128
130
  correction_endpoint_change_not_allowed: '関係の相手は変えられません。今の関係を終えてから、新しい関係を加えてください。',
129
131
  correction_endpoint_type_invalid: 'この組み合わせの関係は加えられません。',
132
+ correction_entity_type_not_allowed: 'ここで作れるのは人と会社だけです。',
133
+ correction_name_required: '名前を書いてください。',
134
+ entity_already_exists: '同じ名前の記録がすでにあります。今の記録を開いて確かめてください。',
130
135
  correction_field_not_allowed: 'この項目は、ここでは直せません。',
131
136
  correction_changes_required: '変更がありません。直したい項目を書き換えてください。',
132
137
  correction_scope_not_allowed: 'この画面では、この記録や関係は直せません。',
@@ -442,6 +447,7 @@ export function readOnlyGraphClient(client) {
442
447
  * `GRAPH_CORRECTION_FIELDS` such as `name` or `goal` (default: every field).
443
448
  * - `edges: false`: relations cannot be corrected or ended.
444
449
  * - `createEdges: false`: relations cannot be added.
450
+ * - `createEntities: false`: people and companies cannot be created.
445
451
  */
446
452
  export function graphCorrectionScope(scope) {
447
453
  const given = isRecord(scope) ? scope : {};
@@ -451,6 +457,7 @@ export function graphCorrectionScope(scope) {
451
457
  return Object.freeze({
452
458
  edges: given.edges !== false,
453
459
  createEdges: given.createEdges !== false,
460
+ createEntities: given.createEntities !== false,
454
461
  /** Whether a record of this kind may be corrected. */
455
462
  entity: (type) => types === null || types.has(type),
456
463
  /** The given record fields that may be corrected, in their order. */
@@ -466,6 +473,7 @@ export function graphEntityCorrectionFields(type) {
466
473
  /** Whether a built correction request stays inside the scope (`recordType` is the corrected entity's kind). */
467
474
  function withinCorrectionScope(scope, body, recordType) {
468
475
  if (body.kind === 'create_edge') return scope.createEdges;
476
+ if (body.kind === 'create_entity') return scope.createEntities && scope.entity(body.entity?.type);
469
477
  if (body.kind === 'update_edge') return scope.edges;
470
478
  const changed = Object.keys(body.changes ?? {});
471
479
  return scope.entity(recordType) && scope.fields(changed).length === changed.length;
@@ -820,6 +828,24 @@ export function buildCorrectionRequest(form) {
820
828
  return { ok: true, body: { kind: 'create_edge', reason, edge } };
821
829
  }
822
830
 
831
+ if (form.kind === 'create_entity') {
832
+ const type = String(form.draft.type ?? '');
833
+ if (!form.entityTypes.includes(type)) return { ok: false, message: '作る記録の種類を選んでください。' };
834
+ const name = String(form.draft.name ?? '').trim();
835
+ if (!name) return { ok: false, message: GRAPH_CORRECTION_ERROR_MESSAGES.correction_name_required };
836
+ const entity = { type, name };
837
+ const aliases = splitAliases(form.draft.aliases ?? '').filter((alias) => alias !== name);
838
+ const summary = String(form.draft.summary ?? '').trim();
839
+ if (aliases.length > 0) entity.aliases = aliases;
840
+ if (summary) entity.summary = summary;
841
+ if (form.draft.validFrom) {
842
+ const validFrom = dayToRfc3339(form.draft.validFrom);
843
+ if (!validFrom) return { ok: false, message: '開始日の形式が正しくありません。' };
844
+ entity.validFrom = validFrom;
845
+ }
846
+ return { ok: true, body: { kind: 'create_entity', reason, entity } };
847
+ }
848
+
823
849
  const changes = {};
824
850
  for (const field of form.fields) {
825
851
  if (field === 'icon') {
@@ -860,11 +886,17 @@ export function buildCorrectionRequest(form) {
860
886
 
861
887
  function readBackFields(form, payload) {
862
888
  if (form.kind === 'create_edge') return ['role', 'context', 'validFrom', 'validTo'];
889
+ if (form.kind === 'create_entity') return ['name', 'aliases', 'summary', 'validFrom'];
863
890
  const changed = Array.isArray(payload?.correction?.changedFields) ? payload.correction.changedFields : [];
864
891
  const fields = changed.map((field) => field.replace(/^metadata\./u, '')).filter((field) => GRAPH_CORRECTION_FIELDS[field]);
865
892
  return fields.length > 0 ? fields : form.fields;
866
893
  }
867
894
 
895
+ /** What a new record is, as rows shown before its values. */
896
+ function createdEntityRows(form, record) {
897
+ return [['種類', form.typeNames[record?.type] ?? typeLabel(record?.type)]];
898
+ }
899
+
868
900
  /** What a new relation connects, as rows shown before its values. */
869
901
  function createdRows(form, record) {
870
902
  const counterpartId = record?.fromId === form.anchor.id ? record?.toId : record?.fromId;
@@ -1015,6 +1047,22 @@ function renderCreateFields(doc, form, callbacks) {
1015
1047
  return group;
1016
1048
  }
1017
1049
 
1050
+ function renderCreateEntityFields(doc, form, callbacks) {
1051
+ const group = makeElement(doc, 'div', { className: 'bb-graph-fields' });
1052
+ if (form.entityTypes.length > 1) {
1053
+ group.append(selectControl(doc, {
1054
+ id: `${form.formId}-type`,
1055
+ name: 'type',
1056
+ label: '種類',
1057
+ value: form.draft.type,
1058
+ options: form.entityTypes.map((type) => ({ value: type, label: form.typeNames[type] ?? typeLabel(type) })),
1059
+ onChange: (value) => callbacks.onInput('type', value),
1060
+ }));
1061
+ }
1062
+ for (const field of form.fields) group.append(fieldControl(doc, form, field, callbacks));
1063
+ return group;
1064
+ }
1065
+
1018
1066
  /**
1019
1067
  * The correction panel. `form` is the state held by `createGraphCorrection`;
1020
1068
  * callbacks are the controller's handlers.
@@ -1035,7 +1083,9 @@ export function renderCorrectionPanel(doc, form, callbacks) {
1035
1083
  const readBack = makeElement(doc, 'div', { className: 'bb-graph-readback', attrs: { 'aria-label': '読み直した内容' } });
1036
1084
  readBack.append(
1037
1085
  makeElement(doc, 'strong', { text: '読み直した内容' }),
1038
- renderValues(doc, payload.record, readBackFields(form, payload), form.kind === 'create_edge' ? createdRows(form, payload.record) : []),
1086
+ renderValues(doc, payload.record, readBackFields(form, payload), form.kind === 'create_edge'
1087
+ ? createdRows(form, payload.record)
1088
+ : form.kind === 'create_entity' ? createdEntityRows(form, payload.record) : []),
1039
1089
  );
1040
1090
  panel.append(readBack, notice(doc, 'accent', mcpNote(payload)));
1041
1091
  panel.append(recordDetails(doc, [['記録ID', payload.record?.id], ['保存した内容のdigest', payload.digest], ['訂正ID', payload.correction?.id]]));
@@ -1045,12 +1095,17 @@ export function renderCorrectionPanel(doc, form, callbacks) {
1045
1095
 
1046
1096
  if (form.phase === 'conflict') {
1047
1097
  const exists = form.conflictCode === 'edge_already_exists';
1048
- panel.append(notice(doc, 'warning', exists
1049
- ? 'この関わりはすでに登録されているため、加えませんでした。今の内容を下に示します。終わった関わりをもう一度始めるときは、その関わりの終了日を消してください。入力は残しています。'
1050
- : 'ほかの保存で内容が変わっていたため、保存しませんでした。今の内容を下に示します。入力は残しています。今の内容を確かめてから、もう一度保存してください。', 'alert'));
1098
+ const entityExists = form.conflictCode === 'entity_already_exists';
1099
+ panel.append(notice(doc, 'warning', entityExists
1100
+ ? '同じ名前の記録がすでにあるため、作りませんでした。今の記録を下に示します。同じ人や会社なら、今の記録を使ってください(終わっている記録は、終了日を消すと戻せます)。入力は残しています。'
1101
+ : exists
1102
+ ? 'この関わりはすでに登録されているため、加えませんでした。今の内容を下に示します。終わった関わりをもう一度始めるときは、その関わりの終了日を消してください。入力は残しています。'
1103
+ : 'ほかの保存で内容が変わっていたため、保存しませんでした。今の内容を下に示します。入力は残しています。今の内容を確かめてから、もう一度保存してください。', 'alert'));
1051
1104
  const current = makeElement(doc, 'div', { className: 'bb-graph-current', attrs: { 'aria-label': '今の内容' } });
1052
- const fields = exists ? ['role', 'context', 'validFrom', 'validTo'] : form.fields;
1053
- current.append(makeElement(doc, 'strong', { text: '今の内容' }), renderValues(doc, form.current.record, fields));
1105
+ const fields = exists ? ['role', 'context', 'validFrom', 'validTo'] : entityExists ? ['name', 'aliases', 'summary', 'validFrom', 'validTo'] : form.fields;
1106
+ current.append(makeElement(doc, 'strong', { text: '今の内容' }), renderValues(doc, form.current.record, fields, entityExists
1107
+ ? [['種類', form.typeNames[form.current.record?.type] ?? typeLabel(form.current.record?.type)]]
1108
+ : []));
1054
1109
  current.append(recordDetails(doc, [['記録ID', form.current.record?.id], ['今の内容のdigest', form.current.digest]]));
1055
1110
  panel.append(current);
1056
1111
  } else if (form.message) {
@@ -1065,6 +1120,8 @@ export function renderCorrectionPanel(doc, form, callbacks) {
1065
1120
  });
1066
1121
  if (form.kind === 'create_edge') {
1067
1122
  formElement.append(renderCreateFields(doc, form, callbacks));
1123
+ } else if (form.kind === 'create_entity') {
1124
+ formElement.append(renderCreateEntityFields(doc, form, callbacks));
1068
1125
  } else {
1069
1126
  const group = makeElement(doc, 'div', { className: 'bb-graph-fields' });
1070
1127
  for (const field of form.fields) group.append(fieldControl(doc, form, field, callbacks));
@@ -1298,6 +1355,29 @@ export function createGraphCorrection({ client, rerender, onSaved, onConflict, n
1298
1355
  void searchCandidates('');
1299
1356
  return form;
1300
1357
  },
1358
+ /**
1359
+ * Create a person or a company. `types` lists the kinds offered (only
1360
+ * `person` and `org` are created; others are dropped), `typeNames` their
1361
+ * labels. Null outside the scope.
1362
+ */
1363
+ openCreateEntity({ types = ['person', 'org'], type, typeNames = {}, title = '記録を加える', subject } = {}) {
1364
+ const entityTypes = allowed.createEntities
1365
+ ? types.filter((candidate) => (candidate === 'person' || candidate === 'org') && allowed.entity(candidate))
1366
+ : [];
1367
+ if (entityTypes.length === 0) return null;
1368
+ const fields = ['name', 'aliases', 'summary', 'validFrom'];
1369
+ form = baseForm('create_entity', {
1370
+ title,
1371
+ subject: subject ?? null,
1372
+ entityTypes,
1373
+ typeNames,
1374
+ fields,
1375
+ original: Object.fromEntries(fields.map((field) => [field, ''])),
1376
+ draft: { type: entityTypes.includes(type) ? type : entityTypes[0], name: '', aliases: '', summary: '', validFrom: '' },
1377
+ });
1378
+ redraw();
1379
+ return form;
1380
+ },
1301
1381
  close() {
1302
1382
  iconReadGeneration += 1;
1303
1383
  form = null;
@@ -1353,7 +1433,7 @@ export function createGraphCorrection({ client, rerender, onSaved, onConflict, n
1353
1433
  // The owner now sees the current record; the next save is based on it
1354
1434
  // and still sends only the fields the owner changed. Untouched fields
1355
1435
  // show the current values so the form never suggests reverting them.
1356
- if (target.kind !== 'create_edge' && typeof result.current.digest === 'string') {
1436
+ if (target.kind !== 'create_edge' && target.kind !== 'create_entity' && typeof result.current.digest === 'string') {
1357
1437
  target.expectedDigest = result.current.digest;
1358
1438
  const currentValues = graphRecordValues(result.current.record);
1359
1439
  for (const field of target.fields) {
@@ -70,6 +70,19 @@ body {
70
70
  gap: 2px;
71
71
  }
72
72
 
73
+ .bb-shell-nav-group {
74
+ display: grid;
75
+ gap: 2px;
76
+ }
77
+
78
+ .bb-shell-nav-group-head {
79
+ margin: 14px 10px 4px;
80
+ color: var(--bb-color-rail-muted);
81
+ font-size: 11px;
82
+ font-weight: 700;
83
+ letter-spacing: .04em;
84
+ }
85
+
73
86
  .bb-shell-nav-link {
74
87
  min-height: 38px;
75
88
  display: flex;
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { createGraphProjectsView } from './graph-projects-view.js';
19
19
  import { createGraphRegistryView } from './graph-registry-view.js';
20
+ import { createPeopleAndCompaniesView } from './people-and-companies-view.js';
20
21
  import { createObjectiveEditorController } from './objective-editor.js';
21
22
  import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
22
23
  import { createJudgmentHistoryUI } from './judgment-history.js';
@@ -42,18 +43,52 @@ const UNREADABLE_LABELS = Object.freeze({
42
43
 
43
44
  // Stroke icons from the organization edition's sprite (apps/web/public/index.html).
44
45
  const NAV_ICON_PATHS = Object.freeze({
45
- 'value-proofs': ['M5 20V10m7 10V4m7 16v-7'],
46
+ 'value-proofs': ['M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5', 'M12 7v5l4 2'],
46
47
  objectives: ['M12 4 3 20h18z', 'M12 9v5m0 3h.01'],
47
48
  projects: ['M3 6.5h7l2 2h9v10H3z', 'M3 6.5v-2h7l2 2'],
48
49
  graph: ['M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1.2 1.2', 'M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1.2-1.2'],
49
50
  minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
51
+ // Two people (the organization edition's 人と会社 uses the same shape).
52
+ people: ['M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M2.5 20a6.5 6.5 0 0 1 13 0', 'M16 4.5a3.5 3.5 0 0 1 0 6.5', 'M18 14a6.5 6.5 0 0 1 3.5 6'],
50
53
  // World view (extension id `world`, ledger P17).
51
54
  world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
52
55
  });
56
+ // Screens without their own icon (for example a private product's extension).
57
+ const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
53
58
  const SVG_NS = 'http://www.w3.org/2000/svg';
54
59
 
60
+ // The menu's areas (brainbase-project ledger D10): the same order and headings
61
+ // as the organization edition. 目的と現状 is the only item of its area, so it
62
+ // has no heading. Inside an area, `order` lists the screens first in that order.
63
+ const NAV_AREAS = Object.freeze([
64
+ Object.freeze({ id: 'purpose', label: null, order: ['objectives'] }),
65
+ Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'] }),
66
+ Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'] }),
67
+ Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'] }),
68
+ Object.freeze({ id: 'settings', label: '設定', order: [] }),
69
+ ]);
70
+
71
+ // Menu order only: the screens' own order (and so the first screen) stays as given.
72
+ // Screens without an area keep their place — those before the first area-bound
73
+ // screen come first (a leading extension such as the world), the rest come last.
74
+ function navLayout(screens) {
75
+ const firstInArea = screens.findIndex((screen) => NAV_AREAS.some((area) => area.id === screen.area));
76
+ if (firstInArea === -1) return { leading: [...screens], areas: [], trailing: [] };
77
+ const leading = screens.slice(0, firstInArea);
78
+ const trailing = screens.slice(firstInArea).filter((screen) => !NAV_AREAS.some((area) => area.id === screen.area));
79
+ const areas = NAV_AREAS.map((area) => {
80
+ const members = screens.filter((screen) => screen.area === area.id);
81
+ const rank = (screen) => {
82
+ const index = area.order.indexOf(screen.id);
83
+ return index === -1 ? area.order.length + members.indexOf(screen) : index;
84
+ };
85
+ return { area, screens: [...members].sort((a, b) => rank(a) - rank(b)) };
86
+ }).filter((entry) => entry.screens.length > 0);
87
+ return { leading, areas, trailing };
88
+ }
89
+
55
90
  function makeIcon(doc, id) {
56
- const paths = NAV_ICON_PATHS[id] ?? NAV_ICON_PATHS['value-proofs'];
91
+ const paths = NAV_ICON_PATHS[id] ?? DEFAULT_NAV_ICON_PATHS;
57
92
  const create = (tag) => (typeof doc.createElementNS === 'function' ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag));
58
93
  const svg = create('svg');
59
94
  svg.setAttribute('class', 'bb-shell-icon');
@@ -235,7 +270,7 @@ function mountObjectives(container, context) {
235
270
  return { editor, worldModel };
236
271
  }
237
272
 
238
- function mountGraphScreen(screenId, createView) {
273
+ function mountGraphScreen(screenId, createView, extra = {}) {
239
274
  return (container, context) => {
240
275
  const viewRoot = makeElement(context.document, 'div');
241
276
  makePage(container, context).append(viewRoot);
@@ -247,16 +282,18 @@ function mountGraphScreen(screenId, createView) {
247
282
  fetcher: context.fetcher,
248
283
  token: context.token,
249
284
  initialEntityId: context.initialEntityId,
285
+ ...extra,
250
286
  });
251
287
  };
252
288
  }
253
289
 
254
290
  export const LOCAL_WEB_SCREENS = Object.freeze([
255
- Object.freeze({ id: 'value-proofs', label: '判断の履歴', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
256
- Object.freeze({ id: 'minutes', label: '議事録', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
257
- Object.freeze({ id: 'objectives', label: '目的と現状', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
258
- Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
259
- Object.freeze({ id: 'graph', label: '情報と関係', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView) }),
291
+ Object.freeze({ id: 'value-proofs', label: '判断の履歴', area: 'outcomes', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
292
+ Object.freeze({ id: 'minutes', label: '議事録', area: 'work', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
293
+ Object.freeze({ id: 'objectives', label: '目的と現状', area: 'purpose', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
294
+ Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', area: 'work', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
295
+ Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView) }),
296
+ Object.freeze({ id: 'people', label: '人と会社', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
260
297
  ]);
261
298
 
262
299
  function renderGraphGate(doc, slot, status, onRecheck, label) {
@@ -373,7 +410,7 @@ export function createLocalWebShell({
373
410
  };
374
411
  menuButton.addEventListener('click', () => setMenuOpen(!sidebar.className.includes('is-open')));
375
412
  backdrop.addEventListener('click', () => setMenuOpen(false));
376
- for (const screen of screens) {
413
+ const navLink = (screen) => {
377
414
  const link = makeElement(doc, 'a', { className: 'bb-shell-nav-link', text: screen.label, attrs: { href: `#${screen.id}` } });
378
415
  // The label stays the link's own text; the icon goes in front where the DOM allows it.
379
416
  if (typeof link.prepend === 'function') link.prepend(makeIcon(doc, screen.id));
@@ -382,8 +419,22 @@ export function createLocalWebShell({
382
419
  controller.show(screen.id);
383
420
  });
384
421
  links.set(screen.id, link);
385
- nav.append(link);
422
+ return link;
423
+ };
424
+ const layout = navLayout(screens);
425
+ for (const screen of layout.leading) nav.append(navLink(screen));
426
+ for (const { area, screens: members } of layout.areas) {
427
+ if (!area.label) {
428
+ for (const screen of members) nav.append(navLink(screen));
429
+ continue;
430
+ }
431
+ const headId = `bb-shell-nav-area-${area.id}`;
432
+ const group = makeElement(doc, 'div', { className: 'bb-shell-nav-group', attrs: { role: 'group', 'aria-labelledby': headId } });
433
+ group.append(makeElement(doc, 'div', { className: 'bb-shell-nav-group-head', text: area.label, attrs: { id: headId } }));
434
+ for (const screen of members) group.append(navLink(screen));
435
+ nav.append(group);
386
436
  }
437
+ for (const screen of layout.trailing) nav.append(navLink(screen));
387
438
  const source = makeElement(doc, 'section', { className: 'bb-shell-source', attrs: { 'aria-label': '出典' } });
388
439
  sidebar.append(brand, nav, source);
389
440
  const workspace = makeElement(doc, 'div', { className: 'bb-shell-workspace' });
@@ -0,0 +1,121 @@
1
+ /*
2
+ * 「人と会社」. Load after workspace-kit.css and graph-view-shared.css.
3
+ * Column templates of the people and company ledgers, the group titles,
4
+ * the write buttons, the recorded company note and the relations in the
5
+ * right rail, with a 760px variant.
6
+ * Uses only the shared tokens in brainbase-tokens.css.
7
+ */
8
+ .bb-pc-people,
9
+ .bb-pc-companies {
10
+ margin-bottom: 30px;
11
+ }
12
+
13
+ .bb-graph .bb-pc-group-title {
14
+ margin: 22px 0 10px;
15
+ color: var(--bb-color-ink);
16
+ font-size: 13px;
17
+ }
18
+
19
+ .bb-graph .bb-pc-company-title {
20
+ margin: 14px 0 8px;
21
+ color: var(--bb-color-muted);
22
+ font-size: 12px;
23
+ font-weight: 650;
24
+ }
25
+
26
+ .bb-graph .bb-pc-empty,
27
+ .bb-graph .bb-pc-ended,
28
+ .bb-graph-rail .bb-pc-empty {
29
+ margin: 0 0 10px;
30
+ color: var(--bb-color-muted);
31
+ font-size: 12px;
32
+ }
33
+
34
+ .bb-pc-people-ledger .bb-ws-ledger-row {
35
+ grid-template-columns: minmax(0, 1.3fr) minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr);
36
+ }
37
+
38
+ .bb-pc-company-ledger .bb-ws-ledger-row {
39
+ grid-template-columns: minmax(0, 1.4fr) minmax(0, .8fr) 84px minmax(0, 1fr) minmax(0, 1fr);
40
+ }
41
+
42
+ .bb-pc-relation-list {
43
+ margin: 0;
44
+ padding: 0;
45
+ list-style: none;
46
+ }
47
+
48
+ .bb-pc-relation {
49
+ display: grid;
50
+ gap: 4px;
51
+ padding: 8px 0;
52
+ border-bottom: 1px solid var(--bb-color-border);
53
+ }
54
+
55
+ .bb-pc-relation.is-ended {
56
+ opacity: .62;
57
+ }
58
+
59
+ .bb-pc-relation-head {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 8px;
63
+ }
64
+
65
+ .bb-pc-relation-meta {
66
+ color: var(--bb-color-muted);
67
+ font-size: 12px;
68
+ }
69
+
70
+ .bb-graph-rail .bb-pc-fix {
71
+ display: grid;
72
+ gap: 6px;
73
+ margin: 16px 0 0;
74
+ color: var(--bb-color-muted);
75
+ font-size: 12px;
76
+ }
77
+
78
+ .bb-pc-actions,
79
+ .bb-pc-relation-actions {
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ gap: 8px;
83
+ }
84
+
85
+ .bb-pc-actions {
86
+ margin: 0 0 12px;
87
+ }
88
+
89
+ .bb-pc-company-cell {
90
+ display: grid;
91
+ gap: 2px;
92
+ }
93
+
94
+ /* A note recorded as text on the person, never a membership: muted and dashed. */
95
+ .bb-pc-org-note {
96
+ justify-self: start;
97
+ padding: 1px 6px;
98
+ border: 1px dashed var(--bb-color-border);
99
+ border-radius: 4px;
100
+ color: var(--bb-color-muted);
101
+ font-size: 11px;
102
+ }
103
+
104
+ @media (max-width: 760px) {
105
+ .bb-pc-people-ledger .bb-ws-ledger-row {
106
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 84px;
107
+ }
108
+
109
+ .bb-pc-people-ledger .bb-ws-ledger-row > :nth-child(4) {
110
+ display: none;
111
+ }
112
+
113
+ .bb-pc-company-ledger .bb-ws-ledger-row {
114
+ grid-template-columns: minmax(0, 1fr) minmax(0, .7fr) 64px;
115
+ }
116
+
117
+ .bb-pc-company-ledger .bb-ws-ledger-row > :nth-child(4),
118
+ .bb-pc-company-ledger .bb-ws-ledger-row > :nth-child(5) {
119
+ display: none;
120
+ }
121
+ }