@unson/brainbase-mcp 0.15.7 → 0.17.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.
- package/dist/cli.js +6 -23
- package/dist/graph-corrections.d.ts +7 -1
- package/dist/graph-corrections.js +89 -4
- package/dist/graph-web-http.d.ts +0 -7
- package/dist/graph-web-http.js +3 -15
- package/dist/graph-web.d.ts +4 -32
- package/dist/graph-web.js +16 -44
- package/dist/local-web-host.d.ts +0 -23
- package/dist/local-web-host.js +4 -15
- package/dist/organization-graph-web.d.ts +5 -27
- package/dist/organization-graph-web.js +4 -128
- package/dist/world-extension.d.ts +4 -19
- package/dist/world-extension.js +15 -75
- package/package.json +3 -2
- package/ui/graph-projects-view.js +4 -3
- package/ui/graph-view-shared.css +49 -0
- package/ui/graph-view-shared.js +156 -46
- package/ui/local-web-shell.js +3 -53
- package/ui/project-icon.js +155 -0
- package/ui/workspace-kit.css +11 -0
- package/ui/workspace-kit.js +5 -2
- package/ui/world/world-view.js +5 -19
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { GraphWebError, openInMemoryGraph } from './graph-web.js';
|
|
5
1
|
import { canonicalEdgeId } from './canonical-graph.js';
|
|
6
2
|
import { canonicalGraphOntologyRelease } from './templates.js';
|
|
7
3
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Projects organization Graph records (as an organization host reads them for
|
|
5
|
+
* its member) into a Graph v2, so shared parts such as the world draw them the
|
|
6
|
+
* same way as a local Graph. This module never reads a network or a token: the
|
|
7
|
+
* host that owns the records passes them in (`projectOrganizationWorld`).
|
|
12
8
|
*
|
|
13
9
|
* Relations come only from what the organization Graph states:
|
|
14
10
|
* - `member_of` person → project → `participates_in` (role = role_code)
|
|
@@ -19,8 +15,6 @@ import { canonicalGraphOntologyRelease } from './templates.js';
|
|
|
19
15
|
*/
|
|
20
16
|
export const ORGANIZATION_GRAPH_WEB_VERSION = 'organization-graph-web.v0';
|
|
21
17
|
const ENTITY_TYPES = ['project', 'person', 'org', 'decision'];
|
|
22
|
-
const FETCH_TIMEOUT_MS = 15_000;
|
|
23
|
-
const CACHE_MS = 60_000;
|
|
24
18
|
function isRecord(value) {
|
|
25
19
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
20
|
}
|
|
@@ -190,121 +184,3 @@ export function projectOrganizationGraph(records, owner) {
|
|
|
190
184
|
excluded: { inactive, unnamed, danglingRelations }
|
|
191
185
|
};
|
|
192
186
|
}
|
|
193
|
-
/** The owner's Brainbase server and token, read at request time (tokens rotate hourly). Never logged or returned. */
|
|
194
|
-
export async function readOrganizationAccess(home = homedir()) {
|
|
195
|
-
try {
|
|
196
|
-
const config = JSON.parse(await readFile(join(home, '.brainbase', 'config.json'), 'utf8'));
|
|
197
|
-
const tokens = JSON.parse(await readFile(join(home, '.brainbase', 'tokens.json'), 'utf8'));
|
|
198
|
-
const server = isRecord(config) ? text(config.server_url) : null;
|
|
199
|
-
const token = isRecord(tokens) ? text(tokens.access_token) : null;
|
|
200
|
-
if (!server || !/^https:\/\/[^/?#@]+$/u.test(server))
|
|
201
|
-
return { reason: 'server_url_missing_or_invalid' };
|
|
202
|
-
if (!token)
|
|
203
|
-
return { reason: 'access_token_missing' };
|
|
204
|
-
return { server, token };
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
return { reason: 'brainbase_auth_files_unreadable' };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function getRecords(request, access, path) {
|
|
211
|
-
const controller = new AbortController();
|
|
212
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
213
|
-
try {
|
|
214
|
-
const response = await request(`${access.server}/api/info/graph/${path}`, {
|
|
215
|
-
method: 'GET',
|
|
216
|
-
headers: { Authorization: `Bearer ${access.token}`, Accept: 'application/json' },
|
|
217
|
-
redirect: 'error',
|
|
218
|
-
signal: controller.signal
|
|
219
|
-
});
|
|
220
|
-
if (response.status === 401 || response.status === 403) {
|
|
221
|
-
throw new GraphWebError('unavailable', 'organization_graph_auth_failed', `The organization Graph refused the owner's token (HTTP ${response.status}); sign in again`);
|
|
222
|
-
}
|
|
223
|
-
if (!response.ok)
|
|
224
|
-
throw new GraphWebError('unavailable', 'organization_graph_unavailable', `The organization Graph answered HTTP ${response.status} for ${path.split('?')[0]}`);
|
|
225
|
-
const body = await response.json();
|
|
226
|
-
if (!isRecord(body) || !Array.isArray(body.records))
|
|
227
|
-
throw new GraphWebError('unavailable', 'organization_graph_unavailable', 'The organization Graph response has no records');
|
|
228
|
-
return body.records;
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (error instanceof GraphWebError)
|
|
232
|
-
throw error;
|
|
233
|
-
const reason = error instanceof Error && error.name === 'AbortError' ? 'timed out' : 'request failed';
|
|
234
|
-
throw new GraphWebError('unavailable', 'organization_graph_unavailable', `The organization Graph ${reason}`);
|
|
235
|
-
}
|
|
236
|
-
finally {
|
|
237
|
-
clearTimeout(timer);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* The organization Graph for the local host (C1). Reads are cached briefly and
|
|
242
|
-
* shared while in flight; a failure is reported as `unavailable`, never as an
|
|
243
|
-
* empty Graph.
|
|
244
|
-
*/
|
|
245
|
-
export async function createOrganizationGraphSource(options = {}) {
|
|
246
|
-
const home = options.home ?? homedir();
|
|
247
|
-
const initial = await readOrganizationAccess(home);
|
|
248
|
-
if ('reason' in initial)
|
|
249
|
-
return initial;
|
|
250
|
-
const request = options.fetch ?? fetch;
|
|
251
|
-
const now = options.now ?? (() => new Date());
|
|
252
|
-
const cacheMs = options.cacheMs ?? CACHE_MS;
|
|
253
|
-
let cached = null;
|
|
254
|
-
let inFlight = null;
|
|
255
|
-
async function load() {
|
|
256
|
-
const access = await readOrganizationAccess(home);
|
|
257
|
-
if ('reason' in access)
|
|
258
|
-
throw new GraphWebError('unavailable', 'organization_graph_not_connected', `The organization Graph cannot be read (${access.reason})`);
|
|
259
|
-
// The organization's words are read alongside; failing to read them never fails the Graph.
|
|
260
|
-
const termsRead = getRecords(request, access, 'entities?type=glossary_term&limit=500')
|
|
261
|
-
.then((records) => projectVocabularyTerms(records))
|
|
262
|
-
.catch(() => null);
|
|
263
|
-
const [project, person, org, decision, raci, memberOf, assignedTo] = await Promise.all([
|
|
264
|
-
getRecords(request, access, 'entities?type=project&limit=500'),
|
|
265
|
-
getRecords(request, access, 'entities?type=person&limit=500'),
|
|
266
|
-
getRecords(request, access, 'entities?type=org&limit=500'),
|
|
267
|
-
getRecords(request, access, 'entities?type=decision&limit=500'),
|
|
268
|
-
getRecords(request, access, 'entities?type=raci_assignment&limit=500'),
|
|
269
|
-
getRecords(request, access, 'edges?type=member_of'),
|
|
270
|
-
getRecords(request, access, 'edges?type=assigned_to')
|
|
271
|
-
]);
|
|
272
|
-
const projection = projectOrganizationGraph({
|
|
273
|
-
entities: {
|
|
274
|
-
project: project,
|
|
275
|
-
person: person,
|
|
276
|
-
org: org,
|
|
277
|
-
decision: decision,
|
|
278
|
-
raci_assignment: raci
|
|
279
|
-
},
|
|
280
|
-
memberOf: memberOf,
|
|
281
|
-
assignedTo: assignedTo
|
|
282
|
-
});
|
|
283
|
-
const source = { dataDir: null, graphFormat: 2, authority: 'organization_graph', server: access.server, readAt: now().toISOString() };
|
|
284
|
-
return { reader: openInMemoryGraph(projection.graph, source), graph: projection.graph, terms: await termsRead };
|
|
285
|
-
}
|
|
286
|
-
async function current() {
|
|
287
|
-
const at = now().getTime();
|
|
288
|
-
if (cached && at - cached.at < cacheMs)
|
|
289
|
-
return cached.loaded;
|
|
290
|
-
inFlight ??= load().then((loaded) => {
|
|
291
|
-
cached = { at: now().getTime(), loaded };
|
|
292
|
-
return loaded;
|
|
293
|
-
}).finally(() => {
|
|
294
|
-
inFlight = null;
|
|
295
|
-
});
|
|
296
|
-
return inFlight;
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
server: initial.server,
|
|
300
|
-
async read() {
|
|
301
|
-
return (await current()).reader;
|
|
302
|
-
},
|
|
303
|
-
async readGraphFile() {
|
|
304
|
-
return (await current()).graph;
|
|
305
|
-
},
|
|
306
|
-
async readVocabularyTerms() {
|
|
307
|
-
return (await current()).terms;
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
}
|
|
@@ -131,11 +131,11 @@ export declare function resolveWorldVocabulary(config: WorldVocabularyConfig | u
|
|
|
131
131
|
/** Reads and checks a vocabulary file; an invalid file is an error, never silently ignored. */
|
|
132
132
|
export declare function readWorldVocabulary(path: string): Promise<WorldVocabularyConfig>;
|
|
133
133
|
/**
|
|
134
|
-
* The businesses from the same Graph the other screens read
|
|
135
|
-
*
|
|
136
|
-
* be read is a state with its reason, never zero businesses.
|
|
134
|
+
* The businesses from the same Graph the other screens read (this Mac's Graph).
|
|
135
|
+
* An organization host draws its own records with `projectOrganizationWorld`.
|
|
136
|
+
* A Graph that cannot be read is a state with its reason, never zero businesses.
|
|
137
137
|
*/
|
|
138
|
-
export declare function readWorldBusinesses(context: Pick<LocalWebModuleContext, 'dataDir'
|
|
138
|
+
export declare function readWorldBusinesses(context: Pick<LocalWebModuleContext, 'dataDir'>, options?: {
|
|
139
139
|
readonly vocabulary?: WorldVocabularyConfig;
|
|
140
140
|
readonly now?: () => Date;
|
|
141
141
|
}): Promise<WorldBusinessesResponse>;
|
|
@@ -174,21 +174,6 @@ export declare function readJudgmentPlaces(journalRoot: string, options?: {
|
|
|
174
174
|
readonly places: readonly WorldJudgmentPlace[];
|
|
175
175
|
readonly reason?: string;
|
|
176
176
|
}>;
|
|
177
|
-
/**
|
|
178
|
-
* The judgments of one organization, for a world drawn from that organization's Graph. A judgment
|
|
179
|
-
* belongs to the organization only when the repository it was worked in is one of the organization's
|
|
180
|
-
* businesses (a registered repository or the business code, the same rule as the world's placement).
|
|
181
|
-
* Every other judgment (another company's work, or one whose repository was not recorded) is withheld:
|
|
182
|
-
* only their count leaves the host, never their questions or repositories.
|
|
183
|
-
*/
|
|
184
|
-
export declare function readOrganizationJudgments(context: Pick<LocalWebModuleContext, 'journalRoot' | 'journalCache' | 'dataDir' | 'now'>, businesses: readonly Pick<WorldBusiness, 'code' | 'repositories'>[], workspaces?: Map<string, string | null>): Promise<{
|
|
185
|
-
readonly status: 'available' | 'unavailable';
|
|
186
|
-
readonly scope: 'organization';
|
|
187
|
-
readonly withheld: number;
|
|
188
|
-
readonly places: readonly WorldJudgmentPlace[];
|
|
189
|
-
readonly home: unknown;
|
|
190
|
-
readonly reason?: string;
|
|
191
|
-
}>;
|
|
192
177
|
export declare function createWorldExtension(options?: {
|
|
193
178
|
readonly vocabulary?: WorldVocabularyConfig;
|
|
194
179
|
}): LocalWebExtension;
|
package/dist/world-extension.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
4
|
-
import { publicHome } from './value-proof-review-http.js';
|
|
3
|
+
import { readJudgmentValueProofJournal, } from './judgment-value-proof-review.js';
|
|
5
4
|
import { projectOrganizationGraph, projectVocabularyTerms } from './organization-graph-web.js';
|
|
6
5
|
import { readLocalGraphState } from './local-web-host.js';
|
|
7
6
|
import { loadPersonalOs } from './ssot.js';
|
|
@@ -224,37 +223,26 @@ export async function readWorldVocabulary(path) {
|
|
|
224
223
|
return { kinds, statuses };
|
|
225
224
|
}
|
|
226
225
|
/**
|
|
227
|
-
* The businesses from the same Graph the other screens read
|
|
228
|
-
*
|
|
229
|
-
* be read is a state with its reason, never zero businesses.
|
|
226
|
+
* The businesses from the same Graph the other screens read (this Mac's Graph).
|
|
227
|
+
* An organization host draws its own records with `projectOrganizationWorld`.
|
|
228
|
+
* A Graph that cannot be read is a state with its reason, never zero businesses.
|
|
230
229
|
*/
|
|
231
230
|
export async function readWorldBusinesses(context, options = {}) {
|
|
232
231
|
const now = (options.now ?? (() => new Date()))();
|
|
233
232
|
let graph;
|
|
234
233
|
let source;
|
|
235
|
-
let terms;
|
|
236
234
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
graph = await context.organizationGraph.readGraphFile();
|
|
242
|
-
source = { authority: 'organization_graph', server: context.organizationGraph.server };
|
|
243
|
-
terms = context.organizationGraph.readVocabularyTerms ? await context.organizationGraph.readVocabularyTerms().catch(() => null) : null;
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
const state = await readLocalGraphState(context.dataDir);
|
|
247
|
-
if (state.status === 'not_initialized' || state.status === 'migration_required') {
|
|
248
|
-
return { status: state.status, version: WORLD_EXTENSION_VERSION, reason: state.status };
|
|
249
|
-
}
|
|
250
|
-
if (state.status !== 'ready')
|
|
251
|
-
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: state.message ?? 'local_graph_unreadable' };
|
|
252
|
-
const os = await loadPersonalOs(context.dataDir);
|
|
253
|
-
if (os.graph.version !== 2)
|
|
254
|
-
return { status: 'migration_required', version: WORLD_EXTENSION_VERSION, reason: 'migration_required' };
|
|
255
|
-
graph = os.graph;
|
|
256
|
-
source = { authority: 'local', dataDir: context.dataDir };
|
|
235
|
+
const state = await readLocalGraphState(context.dataDir);
|
|
236
|
+
if (state.status === 'not_initialized' || state.status === 'migration_required') {
|
|
237
|
+
return { status: state.status, version: WORLD_EXTENSION_VERSION, reason: state.status };
|
|
257
238
|
}
|
|
239
|
+
if (state.status !== 'ready')
|
|
240
|
+
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: state.message ?? 'local_graph_unreadable' };
|
|
241
|
+
const os = await loadPersonalOs(context.dataDir);
|
|
242
|
+
if (os.graph.version !== 2)
|
|
243
|
+
return { status: 'migration_required', version: WORLD_EXTENSION_VERSION, reason: 'migration_required' };
|
|
244
|
+
graph = os.graph;
|
|
245
|
+
source = { authority: 'local', dataDir: context.dataDir };
|
|
258
246
|
}
|
|
259
247
|
catch (error) {
|
|
260
248
|
return { status: 'unavailable', version: WORLD_EXTENSION_VERSION, reason: error instanceof Error ? error.message : 'graph_unreadable' };
|
|
@@ -265,7 +253,7 @@ export async function readWorldBusinesses(context, options = {}) {
|
|
|
265
253
|
version: WORLD_EXTENSION_VERSION,
|
|
266
254
|
source,
|
|
267
255
|
as_of: now.toISOString(),
|
|
268
|
-
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses
|
|
256
|
+
vocabulary: resolveWorldVocabulary(options.vocabulary, projection.businesses),
|
|
269
257
|
...projection,
|
|
270
258
|
};
|
|
271
259
|
}
|
|
@@ -315,44 +303,6 @@ export async function readJudgmentPlaces(journalRoot, options = {}) {
|
|
|
315
303
|
}));
|
|
316
304
|
return { status: 'available', places };
|
|
317
305
|
}
|
|
318
|
-
/**
|
|
319
|
-
* The judgments of one organization, for a world drawn from that organization's Graph. A judgment
|
|
320
|
-
* belongs to the organization only when the repository it was worked in is one of the organization's
|
|
321
|
-
* businesses (a registered repository or the business code, the same rule as the world's placement).
|
|
322
|
-
* Every other judgment (another company's work, or one whose repository was not recorded) is withheld:
|
|
323
|
-
* only their count leaves the host, never their questions or repositories.
|
|
324
|
-
*/
|
|
325
|
-
export async function readOrganizationJudgments(context, businesses, workspaces = new Map()) {
|
|
326
|
-
const journal = await readJudgmentValueProofJournal({ root: context.journalRoot, cache: context.journalCache });
|
|
327
|
-
if (journal.status !== 'available')
|
|
328
|
-
return { status: 'unavailable', scope: 'organization', withheld: 0, places: [], home: null, reason: 'journal_unreadable' };
|
|
329
|
-
const belongs = (workspace) => workspace !== null
|
|
330
|
-
&& businesses.some((business) => business.repositories.includes(workspace) || business.code === workspace);
|
|
331
|
-
const kept = [];
|
|
332
|
-
const places = [];
|
|
333
|
-
for (const entry of journal.entries) {
|
|
334
|
-
const workspace = await entryWorkspace(entry.file, workspaces);
|
|
335
|
-
if (!belongs(workspace))
|
|
336
|
-
continue;
|
|
337
|
-
kept.push(entry);
|
|
338
|
-
places.push({ decision_attempt_id: entry.proof.decision_attempt_id, workspace });
|
|
339
|
-
}
|
|
340
|
-
const [feedback, answers] = await Promise.all([
|
|
341
|
-
readJudgmentValueProofFeedback({ dataDir: context.dataDir }),
|
|
342
|
-
readJudgmentValueProofAnswers({ dataDir: context.dataDir }),
|
|
343
|
-
]);
|
|
344
|
-
// Rejected files are left out too: their names would say nothing about the organization they belong to.
|
|
345
|
-
const home = buildJudgmentValueProofReviewHome({ ...journal, entries: kept, rejected: [] }, feedback, { now: context.now(), answers });
|
|
346
|
-
return { status: 'available', scope: 'organization', withheld: journal.entries.length - kept.length, places, home: publicHome(home) };
|
|
347
|
-
}
|
|
348
|
-
async function entryWorkspace(valueProofFile, workspaces) {
|
|
349
|
-
const turnInputFile = `${valueProofFile.slice(0, -VALUE_PROOF_SUFFIX.length)}.turn-input.json`;
|
|
350
|
-
if (!workspaces.has(turnInputFile)) {
|
|
351
|
-
const turnInput = await readJsonFile(turnInputFile);
|
|
352
|
-
workspaces.set(turnInputFile, isRecord(turnInput) ? text(turnInput.project_code) : null);
|
|
353
|
-
}
|
|
354
|
-
return workspaces.get(turnInputFile) ?? null;
|
|
355
|
-
}
|
|
356
306
|
async function readJsonFile(path) {
|
|
357
307
|
try {
|
|
358
308
|
return JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -375,16 +325,6 @@ function createWorldModule(context, read) {
|
|
|
375
325
|
writeJson(response, 200, await read());
|
|
376
326
|
return true;
|
|
377
327
|
}
|
|
378
|
-
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/organization-judgments`) {
|
|
379
|
-
response.setHeader('Cache-Control', 'no-store');
|
|
380
|
-
const world = await read();
|
|
381
|
-
if (world.status !== 'ok' || world.source.authority !== 'organization_graph') {
|
|
382
|
-
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, status: 'unavailable', scope: 'organization', withheld: 0, places: [], home: null, reason: 'not_reading_organization_graph' });
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readOrganizationJudgments(context, world.businesses, workspaces)) });
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
328
|
if (path === `/api/extensions/${WORLD_EXTENSION_ID}/judgment-places`) {
|
|
389
329
|
response.setHeader('Cache-Control', 'no-store');
|
|
390
330
|
writeJson(response, 200, { version: WORLD_EXTENSION_VERSION, ...(await readJudgmentPlaces(context.journalRoot, { cache: context.journalCache, workspaces })) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
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",
|
|
@@ -228,6 +228,7 @@
|
|
|
228
228
|
"./ui/workspace-kit.css": "./ui/workspace-kit.css",
|
|
229
229
|
"./ui/graph-view-shared": "./ui/graph-view-shared.js",
|
|
230
230
|
"./ui/graph-view-shared.css": "./ui/graph-view-shared.css",
|
|
231
|
+
"./ui/project-icon": "./ui/project-icon.js",
|
|
231
232
|
"./ui/graph-own-share": "./ui/graph-own-share.js",
|
|
232
233
|
"./ui/graph-projects-view": "./ui/graph-projects-view.js",
|
|
233
234
|
"./ui/graph-projects-view.css": "./ui/graph-projects-view.css",
|
|
@@ -358,5 +359,5 @@
|
|
|
358
359
|
"engines": {
|
|
359
360
|
"node": ">=20"
|
|
360
361
|
},
|
|
361
|
-
"gitHead": "
|
|
362
|
+
"gitHead": "78dc17b398f64be355a8a51c7460cfe5c323616a"
|
|
362
363
|
}
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
graphHostEmptyNotice,
|
|
56
56
|
} from './graph-view-shared.js';
|
|
57
57
|
import { graphOwnShareSource, ownProjectDetail, ownShareSection, ownShareStateNotice } from './graph-own-share.js';
|
|
58
|
+
import { renderProjectIcon } from './project-icon.js';
|
|
58
59
|
import {
|
|
59
60
|
createProjectKnowledgeWorkspace,
|
|
60
61
|
normalizeProjectContext,
|
|
@@ -617,7 +618,7 @@ export function createGraphProjectsView({
|
|
|
617
618
|
void reading;
|
|
618
619
|
},
|
|
619
620
|
cells: [
|
|
620
|
-
{ primary: project.name, secondary: project.id },
|
|
621
|
+
{ primary: project.name, secondary: project.id, leading: renderProjectIcon(doc, project.icon, project.name, { size: 'sm' }) },
|
|
621
622
|
textCell(project.goal, '目的未記入'),
|
|
622
623
|
accountableCell(project),
|
|
623
624
|
textCell(project.status, '未記入'),
|
|
@@ -721,7 +722,7 @@ export function createGraphProjectsView({
|
|
|
721
722
|
const project = loaded?.project ?? listItem(state.selectedId) ?? { id: state.selectedId, name: state.selectedId };
|
|
722
723
|
// The record a host's read-only note may point at.
|
|
723
724
|
const noteSubject = { id: project.id, type: 'project', metadata: loaded?.project?.metadata ?? {} };
|
|
724
|
-
const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id })];
|
|
725
|
+
const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id, leading: renderProjectIcon(doc, project.icon, project.name) })];
|
|
725
726
|
if (!loaded) {
|
|
726
727
|
const readState = detail?.id === state.selectedId
|
|
727
728
|
? detail
|
|
@@ -756,7 +757,7 @@ export function createGraphProjectsView({
|
|
|
756
757
|
children.push(renderParticipants(loaded));
|
|
757
758
|
if (writable) {
|
|
758
759
|
const canAdd = scope.createEdges;
|
|
759
|
-
const projectFields = scope.entity('project') ? scope.fields(['name', 'goal', 'status']) : [];
|
|
760
|
+
const projectFields = scope.entity('project') ? scope.fields(['name', 'goal', 'status', 'icon']) : [];
|
|
760
761
|
const actions = [
|
|
761
762
|
canAdd ? workspaceButton(doc, {
|
|
762
763
|
text: '関係者を加える',
|
package/ui/graph-view-shared.css
CHANGED
|
@@ -100,6 +100,55 @@
|
|
|
100
100
|
min-width: 0;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/* Project marks are shared by the ledger, rail and correction readback. The
|
|
104
|
+
unregistered initial is painted from an attribute so it never pollutes the
|
|
105
|
+
ledger's accessible project name or correction history with data URLs. */
|
|
106
|
+
.bb-project-icon {
|
|
107
|
+
display: inline-grid;
|
|
108
|
+
flex: 0 0 36px;
|
|
109
|
+
place-items: center;
|
|
110
|
+
width: 36px;
|
|
111
|
+
height: 36px;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
border: 1px solid var(--bb-color-border);
|
|
114
|
+
border-radius: var(--bb-radius-sm);
|
|
115
|
+
color: var(--bb-color-accent-strong);
|
|
116
|
+
background: var(--bb-color-accent-soft);
|
|
117
|
+
font-size: 15px;
|
|
118
|
+
font-weight: 750;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.bb-project-icon.is-sm {
|
|
123
|
+
flex-basis: 26px;
|
|
124
|
+
width: 26px;
|
|
125
|
+
height: 26px;
|
|
126
|
+
border-radius: 6px;
|
|
127
|
+
font-size: 11px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bb-project-icon::before { content: attr(data-initial); }
|
|
131
|
+
.bb-project-icon.is-registered::before { content: ''; }
|
|
132
|
+
.bb-project-icon img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
133
|
+
|
|
134
|
+
.bb-graph-icon-value,
|
|
135
|
+
.bb-graph-icon-picker {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
flex-wrap: wrap;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: var(--bb-space-2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.bb-graph-icon-picker { margin-top: var(--bb-space-2); }
|
|
143
|
+
.bb-graph-icon-picker-state { color: var(--bb-color-muted); font-size: 12px; }
|
|
144
|
+
.bb-graph-field-error { display: block; color: var(--bb-color-danger); font-size: 12px; }
|
|
145
|
+
.bb-graph-correction .bb-project-icon { margin-right: var(--bb-space-1); }
|
|
146
|
+
|
|
147
|
+
.bb-ws-ledger-object > .bb-project-icon {
|
|
148
|
+
grid-row: 1 / span 2;
|
|
149
|
+
}
|
|
150
|
+
.bb-ws-rail-head > .bb-project-icon { margin-bottom: var(--bb-space-3); }
|
|
151
|
+
|
|
103
152
|
/* Compact items: each 関係者 or relation in the rail. */
|
|
104
153
|
.bb-graph-compact-list {
|
|
105
154
|
display: grid;
|