@unson/brainbase-mcp 0.41.11 → 0.42.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.
@@ -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 { createGraphOntologyView } from './graph-ontology-view.js';
20
21
  import { createPeopleAndCompaniesView } from './people-and-companies-view.js';
21
22
  import { createObjectiveEditorController } from './objective-editor.js';
22
23
  import { createObjectiveEditorHttpPort } from './objective-editor-http-port.js';
@@ -25,8 +26,11 @@ import { createMeetingMinutesUI } from './meeting-minutes.js';
25
26
  import { createMeetingMinutesStorageExtension } from './meeting-minutes-storage.js';
26
27
  import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-http.js';
27
28
  import { createWorldModelView } from './world-model-view.js';
29
+ import { createPersonalMemoryUI, parsePersonalMemoryRoute, personalMemoryRouteTarget } from './personal-memory.js';
30
+ import { createPersonalMemoryHttpPort } from './personal-memory-http-port.js';
28
31
  import { createGraphClient } from './graph-view-shared.js';
29
32
  import { installNativeScrollbars } from './native-scrollbars.js';
33
+ import { browserAreaStorage, createAreaMemory, createAreaNav } from './area-nav.js';
30
34
 
31
35
  export const LOCAL_WEB_SHELL_CONTRACT_VERSION = 'brainbase.local-web-shell.v1';
32
36
 
@@ -51,22 +55,32 @@ const NAV_ICON_PATHS = Object.freeze({
51
55
  minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
52
56
  // Two people (the organization edition's 人と会社 uses the same shape).
53
57
  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'],
58
+ // あなたの記憶 (ledger D15): one person and a lock, seen only by the owner.
59
+ memory: ['M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M3.5 20a6.5 6.5 0 0 1 10-5.5', 'M15 15h6v5h-6z', 'M16.5 15v-1.5a1.5 1.5 0 0 1 3 0V15'],
60
+ // Kinds of information and how they connect (settings, ledger D14): three linked boxes.
61
+ ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
54
62
  // World view (extension id `world`, ledger P17).
55
63
  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'],
64
+ // Areas whose screens have their own icons (ledger D10, stage 2): a briefcase, and
65
+ // the organization edition's gear.
66
+ 'area:work': ['M4 8h16v11H4z', 'M9 8V5h6v3', 'M4 13h16'],
67
+ 'area:settings': ['M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
56
68
  });
57
69
  // Screens without their own icon (for example a private product's extension).
58
70
  const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
59
71
  const SVG_NS = 'http://www.w3.org/2000/svg';
60
72
 
61
- // The menu's areas (brainbase-project ledger D10): the same order and headings
62
- // as the organization edition. 目的と現状 is the only item of its area, so it
63
- // has no heading. Inside an area, `order` lists the screens first in that order.
73
+ // The menu's areas (brainbase-project ledger D10): the same order and names as
74
+ // the organization edition. The left side lists only the areas; the screens of
75
+ // the current area are tabs above the page (stage 2). 目的と現状 is the only
76
+ // screen of its area and keeps its own name. Inside an area, `order` lists the
77
+ // screens first in that order; `defaultScreen` opens when nothing is remembered.
64
78
  const NAV_AREAS = Object.freeze([
65
- Object.freeze({ id: 'purpose', label: null, order: ['objectives'] }),
66
- Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'] }),
67
- Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'] }),
68
- Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'] }),
69
- Object.freeze({ id: 'settings', label: '設定', order: [] }),
79
+ Object.freeze({ id: 'purpose', label: null, order: ['objectives'], icon: 'objectives' }),
80
+ Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'], defaultScreen: 'projects', icon: 'area:work' }),
81
+ Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'], icon: 'value-proofs' }),
82
+ Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people', 'memory'], defaultScreen: 'graph', icon: 'graph' }),
83
+ Object.freeze({ id: 'settings', label: '設定', order: ['ontology'], defaultScreen: 'ontology', icon: 'area:settings' }),
70
84
  ]);
71
85
 
72
86
  // Menu order only: the screens' own order (and so the first screen) stays as given.
@@ -88,6 +102,23 @@ function navLayout(screens) {
88
102
  return { leading, areas, trailing };
89
103
  }
90
104
 
105
+ // Every menu line is an area: a screen without an area is an area of its own.
106
+ function navAreas(screens) {
107
+ const layout = navLayout(screens);
108
+ const own = (screen) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id });
109
+ return [
110
+ ...layout.leading.map(own),
111
+ ...layout.areas.map(({ area, screens: members }) => ({
112
+ id: area.id,
113
+ label: area.label,
114
+ defaultScreen: area.defaultScreen,
115
+ screenIds: members.map((screen) => screen.id),
116
+ icon: area.icon,
117
+ })),
118
+ ...layout.trailing.map(own),
119
+ ];
120
+ }
121
+
91
122
  function makeIcon(doc, id) {
92
123
  const paths = NAV_ICON_PATHS[id] ?? DEFAULT_NAV_ICON_PATHS;
93
124
  const create = (tag) => (typeof doc.createElementNS === 'function' ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag));
@@ -136,11 +167,14 @@ export function parseLocalWebTarget(target) {
136
167
  // プロジェクトと関係者 one project (`project` = Graph project id); other screens ignore them.
137
168
  const decisionId = screenId === 'value-proofs' ? params.get('decision')?.trim() || null : null;
138
169
  const projectId = screenId === 'projects' ? params.get('project')?.trim() || null : null;
170
+ // あなたの記憶 keeps its filters and the opened memory (never the search words).
171
+ const memoryRoute = screenId === 'memory' ? parsePersonalMemoryRoute(params) : null;
139
172
  return {
140
173
  screenId,
141
174
  entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
142
175
  ...(decisionId ? { decisionId } : {}),
143
176
  ...(projectId ? { projectId } : {}),
177
+ ...(memoryRoute ? { memoryRoute } : {}),
144
178
  };
145
179
  }
146
180
 
@@ -201,6 +235,26 @@ function judgmentReferenceLabelReader(context) {
201
235
  };
202
236
  }
203
237
 
238
+ function personalMemoryPort(context) {
239
+ return createPersonalMemoryHttpPort({ fetcher: context.fetcher, token: context.token });
240
+ }
241
+
242
+ /** The opening of each used memory, for 判断の履歴's 本人の考え; the local host has one owner. */
243
+ function personalMemoryResolver(context) {
244
+ const port = personalMemoryPort(context);
245
+ return async (refs) => {
246
+ const list = await port.listMemories({});
247
+ if (list.status !== 'ok') throw new Error(list.reason);
248
+ const wanted = new Set(refs);
249
+ const found = new Map();
250
+ for (const item of list.items) {
251
+ if (!wanted.has(item.id)) continue;
252
+ found.set(item.id, { excerpt: item.text, href: `#${personalMemoryRouteTarget({ memoryId: item.id })}` });
253
+ }
254
+ return found;
255
+ };
256
+ }
257
+
204
258
  function mountToday(container, context) {
205
259
  return createJudgmentHistoryUI({
206
260
  root: makePage(container, context),
@@ -211,6 +265,23 @@ function mountToday(container, context) {
211
265
  token: context.token,
212
266
  basePath: '/api/judgment-history',
213
267
  resolveReferenceLabels: judgmentReferenceLabelReader(context),
268
+ resolvePersonalMemory: personalMemoryResolver(context),
269
+ });
270
+ }
271
+
272
+ function mountPersonalMemory(container, context) {
273
+ return createPersonalMemoryUI({
274
+ root: makePage(container, context),
275
+ rail: context.rail,
276
+ page: context.page,
277
+ document: context.document,
278
+ port: personalMemoryPort(context),
279
+ judgmentHref: (decisionId) => `#value-proofs?decision=${encodeURIComponent(decisionId)}`,
280
+ candidatesCommand: 'brainbase onboard:candidates',
281
+ route: context.initialEntityId ?? undefined,
282
+ onRouteChange: context.navigate
283
+ ? (route, options) => context.navigate(personalMemoryRouteTarget(route), options)
284
+ : undefined,
214
285
  });
215
286
  }
216
287
 
@@ -299,7 +370,7 @@ function mountGraphScreen(screenId, createView, extra = {}) {
299
370
  fetcher: context.fetcher,
300
371
  token: context.token,
301
372
  initialEntityId: context.initialEntityId,
302
- ...extra,
373
+ ...(typeof extra === 'function' ? extra(context) : extra),
303
374
  });
304
375
  };
305
376
  }
@@ -309,8 +380,15 @@ export const LOCAL_WEB_SCREENS = Object.freeze([
309
380
  Object.freeze({ id: 'minutes', label: '議事録', area: 'work', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
310
381
  Object.freeze({ id: 'objectives', label: '目的と現状', area: 'purpose', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
311
382
  Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', area: 'work', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
312
- Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView) }),
383
+ // A record of 情報と関係 opens as its own screen at `#graph?entity_id=…`; the list is `#graph`.
384
+ Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
385
+ onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
386
+ })) }),
313
387
  Object.freeze({ id: 'people', label: '人と会社', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
388
+ // Only the owner sees it (ledger D15); the local host has one owner, so it is always listed here.
389
+ Object.freeze({ id: 'memory', label: 'あなたの記憶', area: 'information', usesGraph: false, rail: true, ownerOnly: true, source: '手元の個人KG', mount: mountPersonalMemory }),
390
+ // The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
391
+ Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', area: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
314
392
  ]);
315
393
 
316
394
  function renderGraphGate(doc, slot, status, onRecheck, label) {
@@ -394,19 +472,26 @@ export function createLocalWebShell({
394
472
  initialScreen,
395
473
  meetingMinutesStorage = null,
396
474
  meetingMinutesLineage = null,
475
+ areaMemory = createAreaMemory(browserAreaStorage()),
476
+ onNavigate,
477
+ // A host that serves more than one person passes false for anyone but the owner:
478
+ // owner-only screens (あなたの記憶) are then neither listed nor mounted.
479
+ isOwner = true,
397
480
  } = {}) {
398
481
  if (!root) throw new TypeError('root is required');
399
482
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
400
483
  if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
484
+ screens = screens.filter((screen) => isOwner === true || screen.ownerOnly !== true);
401
485
  const request = typeof fetcher === 'function'
402
486
  ? fetcher
403
487
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
404
- const context = Object.freeze({ document: doc, fetcher: request, token, meetingMinutesStorage, meetingMinutesLineage });
488
+ // `navigate(target, { back })` lets a screen give its own state an address (the host's hash).
489
+ const navigate = typeof onNavigate === 'function' ? (target, options = {}) => onNavigate(target, options) : null;
490
+ const context = Object.freeze({ document: doc, fetcher: request, token, meetingMinutesStorage, meetingMinutesLineage, navigate });
405
491
 
406
492
  const status = { phase: 'loading', data: null, error: null };
407
493
  const mounted = new Map();
408
494
  const slots = new Map();
409
- const links = new Map();
410
495
  const pendingEntityIds = new Map();
411
496
  let active = null;
412
497
 
@@ -427,37 +512,27 @@ export function createLocalWebShell({
427
512
  };
428
513
  menuButton.addEventListener('click', () => setMenuOpen(!sidebar.className.includes('is-open')));
429
514
  backdrop.addEventListener('click', () => setMenuOpen(false));
430
- const navLink = (screen) => {
431
- const link = makeElement(doc, 'a', { className: 'bb-shell-nav-link', text: screen.label, attrs: { href: `#${screen.id}` } });
432
- // The label stays the link's own text; the icon goes in front where the DOM allows it.
433
- if (typeof link.prepend === 'function') link.prepend(makeIcon(doc, screen.id));
434
- link.addEventListener('click', () => {
515
+ const areaList = navAreas(screens);
516
+ const areaNav = createAreaNav({
517
+ document: doc,
518
+ areas: areaList,
519
+ screens: screens.map((screen) => ({ id: screen.id, label: screen.label })),
520
+ memory: areaMemory,
521
+ linkClassName: 'bb-shell-nav-link',
522
+ iconFor: (area) => makeIcon(doc, areaList.find((entry) => entry.id === area.id)?.icon ?? area.id),
523
+ onSelect: (screenId) => {
435
524
  setMenuOpen(false);
436
- controller.show(screen.id);
437
- });
438
- links.set(screen.id, link);
439
- return link;
440
- };
441
- const layout = navLayout(screens);
442
- for (const screen of layout.leading) nav.append(navLink(screen));
443
- for (const { area, screens: members } of layout.areas) {
444
- if (!area.label) {
445
- for (const screen of members) nav.append(navLink(screen));
446
- continue;
447
- }
448
- const headId = `bb-shell-nav-area-${area.id}`;
449
- const group = makeElement(doc, 'div', { className: 'bb-shell-nav-group', attrs: { role: 'group', 'aria-labelledby': headId } });
450
- group.append(makeElement(doc, 'div', { className: 'bb-shell-nav-group-head', text: area.label, attrs: { id: headId } }));
451
- for (const screen of members) group.append(navLink(screen));
452
- nav.append(group);
453
- }
454
- for (const screen of layout.trailing) nav.append(navLink(screen));
525
+ controller.show(screenId);
526
+ },
527
+ });
528
+ nav.append(areaNav.nav);
455
529
  const source = makeElement(doc, 'section', { className: 'bb-shell-source', attrs: { 'aria-label': '出典' } });
456
530
  sidebar.append(brand, nav, source);
457
531
  const workspace = makeElement(doc, 'div', { className: 'bb-shell-workspace' });
458
532
  const mobileBar = makeElement(doc, 'div', { className: 'bb-shell-mobile-bar' });
459
533
  mobileBar.append(makeElement(doc, 'strong', { text: 'Brainbase' }), menuButton);
460
534
  const main = makeElement(doc, 'main', { className: 'bb-shell-main' });
535
+ main.append(areaNav.tabs);
461
536
  for (const screen of screens) {
462
537
  const slot = makeElement(doc, 'section', { className: 'bb-shell-screen', attrs: { 'data-screen': screen.id, 'aria-label': screen.label } });
463
538
  slot.hidden = true;
@@ -493,8 +568,11 @@ export function createLocalWebShell({
493
568
  if (mounted.has(screen.id)) {
494
569
  const view = mounted.get(screen.id);
495
570
  if (entityId && screen.id === 'graph' && typeof view?.openEntity === 'function') void view.openEntity(entityId);
571
+ // `#graph` without a record is the list: a record still open there closes (the browser's back).
572
+ if (!entityId && screen.id === 'graph' && view?.state?.detail && typeof view.closeEntity === 'function') view.closeEntity();
496
573
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
497
574
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
575
+ if (entityId && screen.id === 'memory' && typeof view?.applyRoute === 'function') view.applyRoute(entityId);
498
576
  return;
499
577
  }
500
578
  const slot = slots.get(screen.id);
@@ -508,6 +586,7 @@ export function createLocalWebShell({
508
586
  const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
509
587
  const view = screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true;
510
588
  mounted.set(screen.id, view);
589
+ if (screen.id === 'memory' && typeof view?.load === 'function') void view.load();
511
590
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
512
591
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
513
592
  updateRail();
@@ -523,13 +602,12 @@ export function createLocalWebShell({
523
602
  const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
524
603
  pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
525
604
  : screen.id === 'value-proofs' ? parsed.decisionId ?? null
526
- : screen.id === 'projects' ? parsed.projectId ?? null : null);
605
+ : screen.id === 'projects' ? parsed.projectId ?? null
606
+ // あなたの記憶 receives its whole route (filters and the opened memory).
607
+ : screen.id === 'memory' ? parsed.memoryRoute ?? null : null);
527
608
  active = screen.id;
528
609
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
529
- for (const [screenId, link] of links) {
530
- if (screenId === screen.id) link.setAttribute('aria-current', 'page');
531
- else link.removeAttribute('aria-current');
532
- }
610
+ areaNav.show(screen.id);
533
611
  ensureMounted(screen, pendingEntityIds.get(screen.id));
534
612
  updateRail();
535
613
  return controller;
@@ -84,6 +84,9 @@ const SCOPE_LABELS = Object.freeze({
84
84
 
85
85
  const LIFECYCLE_STATUS_LABELS = Object.freeze({
86
86
  active: '有効',
87
+ decided: '決定済み',
88
+ active_reference: '参照用',
89
+ unknown: UNKNOWN,
87
90
  inactive: '無効',
88
91
  retired: '失効',
89
92
  superseded: '置換済み',
@@ -1480,7 +1483,8 @@ export function createKnowledgeOverviewController(options = {}) {
1480
1483
  const { root, api, onSelectProject } = options;
1481
1484
  if (!root || typeof root.replaceChildren !== 'function') throw new Error('knowledge_root_unavailable');
1482
1485
  const labels = resolveKnowledgeUiLabels(options.labels);
1483
- const itemsBase = optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`;
1486
+ // A host path may already carry conditions (for example `?limit=100`); keep them and add the filters.
1487
+ const [itemsBase, itemsBaseQuery = ''] = (optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`).split(/\?(.*)/s);
1484
1488
  const state = {
1485
1489
  filters: { q: '', scope: 'all', status: 'active' },
1486
1490
  list: { state: 'loading', records: null, absence_confirmed: false, searched_scope: null },
@@ -1491,7 +1495,7 @@ export function createKnowledgeOverviewController(options = {}) {
1491
1495
  const projectName = (code) => projectOptions(options.projects).find((option) => option.code === code)?.name ?? null;
1492
1496
 
1493
1497
  function itemsPath() {
1494
- const query = new URLSearchParams();
1498
+ const query = new URLSearchParams(itemsBaseQuery);
1495
1499
  if (state.filters.q) query.set('q', state.filters.q);
1496
1500
  if (state.filters.scope) query.set('scope', state.filters.scope);
1497
1501
  if (state.filters.status) query.set('status', state.filters.status);
@@ -1,8 +1,9 @@
1
1
  /*
2
2
  * 「人と会社」. Load after workspace-kit.css and graph-view-shared.css.
3
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.
4
+ * the add buttons, the recorded company note, and a record's page (the way
5
+ * back, its head, the actions gathered under it and its relations), with a
6
+ * 760px variant.
6
7
  * Uses only the shared tokens in brainbase-tokens.css.
7
8
  */
8
9
  .bb-pc-people,
@@ -67,25 +68,12 @@
67
68
  font-size: 12px;
68
69
  }
69
70
 
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
71
  .bb-pc-relation-actions {
80
72
  display: flex;
81
73
  flex-wrap: wrap;
82
74
  gap: 8px;
83
75
  }
84
76
 
85
- .bb-pc-actions {
86
- margin: 0 0 12px;
87
- }
88
-
89
77
  .bb-pc-company-cell {
90
78
  display: grid;
91
79
  gap: 2px;
@@ -102,6 +90,11 @@
102
90
  }
103
91
 
104
92
  @media (max-width: 760px) {
93
+ /* Below a host's top bar on a phone. */
94
+ .bb-pc-detail {
95
+ scroll-margin-top: 72px;
96
+ }
97
+
105
98
  .bb-pc-people-ledger .bb-ws-ledger-row {
106
99
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 84px;
107
100
  }
@@ -262,3 +255,161 @@
262
255
  align-items: center;
263
256
  min-height: 46px;
264
257
  }
258
+
259
+ /* A record's page (2026-10-16). Choosing a row turns the workspace into the
260
+ record's page, as a CRM does: the way back with the record's place, the
261
+ head, then what can be done to it, gathered in one place with what the page
262
+ says about it (nothing is deleted; why an action is disabled). */
263
+ .bb-pc-action-group {
264
+ display: grid;
265
+ gap: 8px;
266
+ margin: 0 0 20px;
267
+ }
268
+
269
+ .bb-pc-action-row {
270
+ display: flex;
271
+ flex-wrap: wrap;
272
+ gap: 8px;
273
+ }
274
+
275
+ .bb-pc-action-row .bb-ws-button.is-primary {
276
+ border-color: var(--bb-color-action);
277
+ background: var(--bb-color-action);
278
+ color: var(--bb-color-action-text);
279
+ }
280
+
281
+ .bb-pc-action-row .bb-ws-button.is-primary:hover:not(:disabled) {
282
+ border-color: var(--bb-color-action-hover);
283
+ background: var(--bb-color-action-hover);
284
+ }
285
+
286
+ .bb-pc-action-row .bb-ws-button[data-adds]::before {
287
+ content: '+';
288
+ }
289
+
290
+ .bb-pc-action-note,
291
+ .bb-pc-action-reason {
292
+ margin: 0;
293
+ color: var(--bb-color-muted);
294
+ font-size: 12px;
295
+ line-height: 1.6;
296
+ }
297
+
298
+ .bb-pc-action-reason {
299
+ justify-self: start;
300
+ padding: 4px 10px;
301
+ border-radius: 6px;
302
+ background: var(--bb-color-warning-soft);
303
+ color: var(--bb-color-warning);
304
+ }
305
+
306
+ .bb-pc-action-note a {
307
+ color: var(--bb-color-accent-strong);
308
+ }
309
+
310
+ .bb-pc-add {
311
+ margin-top: -4px;
312
+ }
313
+
314
+ .bb-pc-detail {
315
+ display: grid;
316
+ gap: 0;
317
+ scroll-margin-top: 16px;
318
+ }
319
+
320
+ .bb-pc-detail-nav {
321
+ display: flex;
322
+ flex-wrap: wrap;
323
+ align-items: center;
324
+ gap: 6px;
325
+ margin: 0 0 20px;
326
+ padding-bottom: 12px;
327
+ border-bottom: 1px solid var(--bb-color-border);
328
+ }
329
+
330
+ .bb-pc-detail-nav .bb-pc-back {
331
+ margin-right: auto;
332
+ }
333
+
334
+ .bb-pc-detail-place {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 6px;
338
+ margin-right: 4px;
339
+ color: var(--bb-color-muted);
340
+ font-size: 12px;
341
+ }
342
+
343
+ .bb-pc-detail-nav .bb-ws-button[aria-label='前へ'],
344
+ .bb-pc-detail-nav .bb-ws-button[aria-label='次へ'] {
345
+ min-width: 32px;
346
+ padding-inline: 8px;
347
+ }
348
+
349
+ .bb-pc-detail-head {
350
+ display: flex;
351
+ align-items: flex-start;
352
+ gap: 14px;
353
+ margin: 0 0 18px;
354
+ scroll-margin-top: 16px;
355
+ }
356
+
357
+ .bb-pc-mark.bb-pc-detail-mark {
358
+ width: 44px;
359
+ height: 44px;
360
+ margin: 0;
361
+ font-size: 18px;
362
+ }
363
+
364
+ .bb-pc-company-mark.bb-pc-detail-mark {
365
+ border-radius: 10px;
366
+ }
367
+
368
+ .bb-pc-detail-copy {
369
+ display: grid;
370
+ gap: 4px;
371
+ min-width: 0;
372
+ }
373
+
374
+ .bb-pc-detail-copy h2 {
375
+ margin: 0;
376
+ color: var(--bb-color-ink);
377
+ font-size: 22px;
378
+ line-height: 1.3;
379
+ overflow-wrap: anywhere;
380
+ scroll-margin-top: 16px;
381
+ }
382
+
383
+ .bb-pc-detail-copy h2:focus {
384
+ outline: none;
385
+ }
386
+
387
+ .bb-pc-detail-sub {
388
+ margin: 0;
389
+ color: var(--bb-color-muted);
390
+ font-size: 13px;
391
+ }
392
+
393
+ .bb-pc-detail-lead {
394
+ margin: 4px 0 0;
395
+ color: var(--bb-color-ink);
396
+ font-size: 13px;
397
+ line-height: 1.6;
398
+ }
399
+
400
+ .bb-pc-detail-actions {
401
+ padding: 14px 16px;
402
+ border: 1px solid var(--bb-color-border);
403
+ border-radius: 10px;
404
+ background: var(--bb-color-canvas);
405
+ }
406
+
407
+ .bb-pc-detail-block {
408
+ margin: 0 0 22px;
409
+ }
410
+
411
+ .bb-pc-detail-block h3 {
412
+ margin: 0 0 6px;
413
+ color: var(--bb-color-ink);
414
+ font-size: 13px;
415
+ }