@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15
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/CHANGELOG.md +283 -0
- package/README.md +102 -52
- package/SECURITY.md +1 -1
- package/TRADEMARKS.md +2 -2
- package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
- package/contracts/atelier-graph-view.v1.schema.json +1085 -0
- package/contracts/public-api-baseline.json +298 -19
- package/docs/README.md +90 -0
- package/docs/blocks/capability-stewardship.md +1 -1
- package/docs/blocks/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/blocks/learning-harnesses.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
- package/docs/guides/co-authoring.md +113 -0
- package/docs/{distributions.md → guides/distributions.md} +13 -8
- package/docs/guides/extensions.md +189 -0
- package/docs/guides/graph-refresh.md +152 -0
- package/docs/guides/graph-workspace.md +64 -0
- package/docs/{ingestion.md → guides/ingestion.md} +16 -15
- package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +370 -23
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
- package/docs/{research-starter.md → guides/research-starter.md} +1 -1
- package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
- package/docs/{upgrade.md → guides/upgrade.md} +197 -11
- package/docs/knowledge-health/assessment/capabilities.md +6 -15
- package/docs/knowledge-health/facilitator.md +15 -4
- package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
- package/docs/knowledge-health/quickstart.md +97 -75
- package/docs/knowledge-health/readme.md +13 -5
- package/docs/maintainers/describing-the-atelier.md +173 -0
- package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
- package/docs/{announcements.md → reference/announcements.md} +1 -1
- package/docs/{architecture.md → reference/architecture.md} +31 -5
- package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
- package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
- package/docs/{coordination.md → reference/coordination.md} +1 -1
- package/docs/{decisions.md → reference/decisions.md} +3 -3
- package/docs/{design.md → reference/design.md} +1 -1
- package/docs/reference/edge-policy.md +151 -0
- package/docs/reference/graph-view-structured.md +119 -0
- package/docs/reference/graph-view.md +139 -0
- package/docs/reference/graph-workspace.md +37 -0
- package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
- package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
- package/docs/{local-services.md → reference/local-services.md} +43 -4
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
- package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
- package/docs/reference/portability.md +92 -0
- package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
- package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
- package/docs/{project-options.md → reference/project-options.md} +17 -0
- package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
- package/docs/{template-profile.md → reference/template-profile.md} +1 -1
- package/docs/{trackables.md → reference/trackables.md} +2 -2
- package/docs/start/independent-start.md +89 -0
- package/docs/{install.md → start/install.md} +9 -9
- package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
- package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
- package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
- package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
- package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
- package/fixtures/graph-view/synthetic-view.v1.json +1 -0
- package/fixtures/knowledge-health/workshop/README.md +1 -1
- package/fixtures/templates/local-library.v1.json +3 -1
- package/package.json +22 -8
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/claude/atelier-local-service/SKILL.md +1 -1
- package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
- package/skills/claude/mnstry-readiness/SKILL.md +2 -2
- package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/codex/atelier-local-service/SKILL.md +1 -1
- package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
- package/skills/codex/mnstry-readiness/SKILL.md +2 -2
- package/src/architecture/catalog.mjs +2 -2
- package/src/attestation/sign.mjs +1 -1
- package/src/boundary/content-rules.mjs +534 -68
- package/src/boundary/policy.mjs +187 -60
- package/src/cli/run.mjs +26 -9
- package/src/commands/distribution.mjs +1 -1
- package/src/commands/extension.mjs +27 -0
- package/src/commands/ingest.mjs +1 -1
- package/src/commands/learn.mjs +1 -1
- package/src/commands/obsidian.mjs +49 -21
- package/src/commands/review.mjs +3 -0
- package/src/edge-policy/contract.mjs +69 -0
- package/src/edge-policy/policy.mjs +142 -0
- package/src/edge-policy/source-citation-adapter.mjs +43 -0
- package/src/extension-packs/loader.mjs +4 -3
- package/src/extensions/admission.mjs +83 -0
- package/src/extensions/client.d.mts +23 -0
- package/src/extensions/client.mjs +99 -0
- package/src/extensions/contracts.d.mts +29 -0
- package/src/extensions/contracts.mjs +163 -0
- package/src/extensions/convex.d.mts +24 -0
- package/src/extensions/convex.mjs +189 -0
- package/src/extensions/delegation.mjs +54 -0
- package/src/extensions/drizzle.d.mts +11 -0
- package/src/extensions/drizzle.mjs +33 -0
- package/src/extensions/postgres.d.mts +7 -0
- package/src/extensions/postgres.mjs +66 -0
- package/src/extensions/prisma.d.mts +13 -0
- package/src/extensions/prisma.mjs +63 -0
- package/src/extensions/project.mjs +432 -0
- package/src/extensions/scaffold-convex.mjs +130 -0
- package/src/extensions/scaffold-sql.mjs +144 -0
- package/src/extensions/server.d.mts +20 -0
- package/src/extensions/server.mjs +102 -0
- package/src/extensions/sql-scope.mjs +22 -0
- package/src/graph/graph.mjs +13 -5
- package/src/graph/knowledge-graph.mjs +20 -2
- package/src/graph/repo-contribution.mjs +405 -0
- package/src/graph/repo-contribution.types.mjs +32 -0
- package/src/graph-access/edge-decision-adapter.mjs +41 -0
- package/src/graph-access/edge-record-adapter.mjs +73 -0
- package/src/graph-access/index.mjs +253 -0
- package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
- package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
- package/src/graph-access/source-adapter.mjs +81 -0
- package/src/graph-refresh/compare.mjs +110 -0
- package/src/graph-refresh/controller.mjs +324 -0
- package/src/graph-refresh/index.mjs +3 -0
- package/src/graph-refresh/local.mjs +78 -0
- package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
- package/src/graph-view/babylon/index.mjs +2 -0
- package/src/graph-view/babylon/navigator.mjs +312 -0
- package/src/graph-view/babylon/provenance.mjs +36 -0
- package/src/graph-view/babylon/scene-controller.mjs +131 -0
- package/src/graph-view/babylon/semantic-picking.mjs +35 -0
- package/src/graph-view/babylon/web-host.mjs +188 -0
- package/src/graph-view/cli.mjs +3 -0
- package/src/graph-view/command.mjs +34 -0
- package/src/graph-view/contracts.mjs +79 -0
- package/src/graph-view/errors.mjs +5 -0
- package/src/graph-view/index.mjs +4 -0
- package/src/graph-view/intents.mjs +30 -0
- package/src/graph-view/query.mjs +213 -0
- package/src/graph-view/refs.mjs +2 -0
- package/src/graph-view/structured/browser.mjs +262 -0
- package/src/graph-view/structured/styles.mjs +8 -0
- package/src/graph-workspace/authoring.mjs +185 -0
- package/src/graph-workspace/browser/constellation.mjs +369 -0
- package/src/graph-workspace/browser/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/review.mjs +55 -0
- package/src/graph-workspace/browser/styles.mjs +11 -0
- package/src/graph-workspace/browser/workbench.mjs +351 -0
- package/src/graph-workspace/browser/workspace.mjs +431 -0
- package/src/graph-workspace/disclosure.mjs +39 -0
- package/src/graph-workspace/geometry.mjs +78 -0
- package/src/graph-workspace/index.mjs +11 -0
- package/src/graph-workspace/labels.mjs +50 -0
- package/src/graph-workspace/model.mjs +109 -0
- package/src/graph-workspace/recipes.mjs +88 -0
- package/src/graph-workspace/scale.mjs +95 -0
- package/src/graph-workspace/theme.mjs +156 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -0
- package/src/knowledge/ontology-guide.mjs +152 -0
- package/src/knowledge-health/proof/installed-check.mjs +2 -2
- package/src/knowledge-health/rule-identity.mjs +14 -9
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/index.mjs +1 -1
- package/src/personal-workspace/selection.mjs +34 -3
- package/src/project/config.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/project/providers/github.mjs +24 -0
- package/src/project/repo-identity.mjs +3 -22
- package/src/projection/obsidian/contracts.mjs +1 -1
- package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
- package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +84 -5
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/proposals/adapter.mjs +46 -23
- package/src/projection/obsidian/proposals/observation.mjs +40 -2
- package/src/projection/obsidian/publication/publisher.mjs +96 -8
- package/src/projection/project.mjs +21 -16
- package/src/readiness/readiness.mjs +20 -11
- package/src/readiness-protocols/bundled-pack.mjs +14 -766
- package/src/readiness-protocols/bundled-selection.mjs +32 -0
- package/src/readiness-protocols/evidence.mjs +2 -1
- package/src/readiness-protocols/packs/mnstry.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +43 -6
- package/src/runtime/obsidian/engine.mjs +387 -76
- package/src/runtime/obsidian/lifecycle.mjs +20 -15
- package/src/runtime/obsidian/login-item.mjs +6 -3
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +4 -3
- package/src/runtime/obsidian/personal-selection.mjs +187 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- package/src/runtime/obsidian/private-lock.mjs +50 -6
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +68 -16
- package/src/runtime/obsidian/service.mjs +80 -6
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +335 -0
- package/src/ui/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -0
- package/src/upgrade/upgrade.mjs +42 -11
- package/templates/distribution-workspace/README.md +2 -2
- package/templates/external-project-workspace/atelier.project.json +3 -0
- package/templates/external-project-workspace/pack-lifecycle.json +3 -1
- package/templates/knowledge-workspace/README.md +2 -2
- package/docs/integration-and-review-plan.md +0 -409
- package/docs/integration-contract-decisions.md +0 -44
- package/docs/knowledge-health/devday/facilitator.md +0 -16
- package/docs/knowledge-health/devday/public-default.md +0 -107
- package/docs/knowledge-health/devday/quickstart.md +0 -18
- package/docs/knowledge-health/devday/support.md +0 -22
- package/docs/knowledge-health/exercise.md +0 -17
- package/docs/knowledge-health/support.md +0 -34
- package/docs/presentation-review-disposition.md +0 -47
- package/docs/progressive-ingestion-acceptance.md +0 -89
- package/docs/progressive-ingestion.md +0 -235
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{local-review.md → guides/local-review.md} +0 -0
- /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
- /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
- /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
- /package/docs/{attestation.md → reference/attestation.md} +0 -0
- /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
- /package/docs/{continuity.md → reference/continuity.md} +0 -0
- /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
- /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
- /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
- /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
- /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
- /package/docs/{local-state.md → reference/local-state.md} +0 -0
- /package/docs/{ontology.md → reference/ontology.md} +0 -0
- /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
- /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
- /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// The workbench: a sparse graph of rich cards beside a focused authoring sheet.
|
|
2
|
+
// One subject at a time (record, concept or plan relation). The sheet follows the
|
|
3
|
+
// focused authoring register: quiet header, step rail with counts, one step in view,
|
|
4
|
+
// one primary action, deferral, and an honest "saved in this browser" status.
|
|
5
|
+
import { h, clear } from './dom.mjs'
|
|
6
|
+
import { conceptSteps, relationSteps, guideConcept, guideRelation, stepState } from '../authoring.mjs'
|
|
7
|
+
|
|
8
|
+
const VERB = { depends_on: 'depends on', supports: 'supports', contradicts: 'challenges', evidences: 'is evidence about', related: 'links to', supersedes: 'replaces', belongs_to: 'belongs to', implements: 'puts into practice', declares: 'declares' }
|
|
9
|
+
const verb = p => Object.hasOwn(VERB, p) ? VERB[p] : p.replaceAll('_', ' ')
|
|
10
|
+
const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
11
|
+
|
|
12
|
+
export function createWorkbench({ root, model, palette, store, vocab, onBack, onReview, onShowOnMap }) {
|
|
13
|
+
const byId = new Map(model.nodes.map(n => [n.id, n]))
|
|
14
|
+
const zoneIndex = new Map(model.zones.map(z => [z.id, z.index]))
|
|
15
|
+
const zoneColor = id => palette.zones[(zoneIndex.get(id) ?? 0) % palette.zones.length]
|
|
16
|
+
const plan = model.plan ?? { concepts: [], relations: [], questions: [] }
|
|
17
|
+
const conceptById = new Map(plan.concepts.map(c => [c.id, c]))
|
|
18
|
+
const visible = new Set(model.nodes.map(n => n.id))
|
|
19
|
+
let subject = null
|
|
20
|
+
let trail = []
|
|
21
|
+
let step = 0
|
|
22
|
+
let lastLayout = null
|
|
23
|
+
let unsubscribe = null
|
|
24
|
+
const adjacency = new Map()
|
|
25
|
+
for (const e of model.edges) {
|
|
26
|
+
for (const [k, other, dir] of [[e.source, e.target, 'out'], [e.target, e.source, 'in']]) {
|
|
27
|
+
if (!adjacency.has(k)) adjacency.set(k, [])
|
|
28
|
+
adjacency.get(k).push({ edge: e, other, dir })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const head = h('header', { class: 'wb-head' })
|
|
33
|
+
const graphHost = h('div', { class: 'wb-graph', role: 'group', 'aria-label': 'Subject and its closest connections' })
|
|
34
|
+
const edgeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
35
|
+
edgeSvg.setAttribute('class', 'wb-edges'); edgeSvg.setAttribute('aria-hidden', 'true')
|
|
36
|
+
const cardLayer = h('div', { class: 'wb-cards' })
|
|
37
|
+
graphHost.append(edgeSvg, cardLayer)
|
|
38
|
+
const sheet = h('aside', { class: 'wb-sheet', 'aria-labelledby': 'wb-title' })
|
|
39
|
+
root.replaceChildren(head, h('div', { class: 'wb-body' }, graphHost, sheet))
|
|
40
|
+
new ResizeObserver(() => { if (subject && !root.hidden) layoutGraph(false) }).observe(graphHost)
|
|
41
|
+
|
|
42
|
+
// ── Subject neighbourhoods ──
|
|
43
|
+
function neighboursOf(s) {
|
|
44
|
+
if (s.kind === 'record') {
|
|
45
|
+
const list = (adjacency.get(s.id) ?? []).map(a => ({ kind: 'record', id: a.other, predicate: a.edge.type, dir: a.dir, structural: (a.edge.kind === 'relation') }))
|
|
46
|
+
const unique = []
|
|
47
|
+
const seen = new Set()
|
|
48
|
+
list.sort((a, b) => Number(b.structural) - Number(a.structural) || (byId.get(b.id).degree - byId.get(a.id).degree))
|
|
49
|
+
for (const n of list) if (!seen.has(n.id)) { seen.add(n.id); unique.push(n) }
|
|
50
|
+
const node = byId.get(s.id)
|
|
51
|
+
const concepts = node.concepts.filter(id => conceptById.has(id)).map(id => ({ kind: 'concept', id, predicate: 'declares', dir: 'out', structural: true }))
|
|
52
|
+
return { shown: [...concepts, ...unique].slice(0, 10), total: unique.length + concepts.length }
|
|
53
|
+
}
|
|
54
|
+
if (s.kind === 'concept') {
|
|
55
|
+
const rel = plan.relations.filter(r => r.from === s.id || r.to === s.id).map(r => ({ kind: 'concept', id: r.from === s.id ? r.to : r.from, predicate: r.predicate, dir: r.from === s.id ? 'out' : 'in', structural: true, relation: r.id }))
|
|
56
|
+
const declared = (conceptById.get(s.id)?.declaredBy ?? []).filter(id => byId.has(id)).map(id => ({ kind: 'record', id, predicate: 'declares', dir: 'in', structural: true }))
|
|
57
|
+
return { shown: [...rel, ...declared].slice(0, 10), total: rel.length + declared.length }
|
|
58
|
+
}
|
|
59
|
+
const r = plan.relations.find(x => x.id === s.id)
|
|
60
|
+
return { shown: r ? [{ kind: 'concept', id: r.to, predicate: r.predicate, dir: 'out', structural: true, relation: r.id }] : [], total: 1, relation: r }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function card(item, role) {
|
|
64
|
+
const isConcept = item.kind === 'concept'
|
|
65
|
+
const n = isConcept ? null : byId.get(item.id)
|
|
66
|
+
const c = isConcept ? conceptById.get(item.id) : null
|
|
67
|
+
const cited = !isConcept && Object.values(store.data.concepts).some(d => d.evidence.some(b => b.id === item.id))
|
|
68
|
+
const draft = isConcept ? store.conceptDraft(item.id) : undefined
|
|
69
|
+
const proposed = isConcept && draft && (draft.definition !== c.definition || draft.identityRule !== c.identityRule)
|
|
70
|
+
const el = h('button', { class: `wb-card wb-${role} ${isConcept ? 'is-concept' : 'is-record'}`, type: 'button', dataset: { kind: item.kind, id: item.id },
|
|
71
|
+
'aria-label': isConcept ? `${c.title}, concept` : `${n.title}, ${vocab.record} in ${n.zoneLabel}` },
|
|
72
|
+
h('span', { class: 'wb-kind' }, h('i', { class: isConcept ? 'dot-concept' : 'dot', style: isConcept ? '' : `background:${zoneColor(n.zone)}` }), isConcept ? 'Concept' : n.zoneLabel),
|
|
73
|
+
h('span', { class: 'wb-title', text: isConcept ? c.title : n.title }),
|
|
74
|
+
h('span', { class: 'wb-summary', text: isConcept ? (draft?.definition || c.definition || '') : n.summary }),
|
|
75
|
+
h('span', { class: 'wb-meta' }, isConcept ? `${c.declaredBy.length} declaring ${c.declaredBy.length === 1 ? vocab.record : vocab.records}` : `${n.type}${n.status ? ' · ' + n.status : ''} · ${n.degree} links`,
|
|
76
|
+
cited ? h('b', { class: 'tag tag-cited', text: 'Cited' }) : null, proposed ? h('b', { class: 'tag tag-proposed', text: 'Proposed' }) : null))
|
|
77
|
+
if (role === 'neighbour') el.addEventListener('click', () => walk({ kind: item.kind, id: item.id }, el))
|
|
78
|
+
return el
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function layoutGraph(animate) {
|
|
82
|
+
const w = graphHost.clientWidth, hgt = graphHost.clientHeight
|
|
83
|
+
const { shown, total, relation } = neighboursOf(subject)
|
|
84
|
+
// A relayout (resize, citation) rebuilds the cards; keep keyboard focus where it was.
|
|
85
|
+
const active = cardLayer.contains(document.activeElement) ? document.activeElement : null
|
|
86
|
+
const refocus = active && { kind: active.dataset.kind, id: active.dataset.id, role: active.classList.contains('wb-center') ? 'center' : 'neighbour' }
|
|
87
|
+
clear(cardLayer)
|
|
88
|
+
while (edgeSvg.firstChild) edgeSvg.firstChild.remove()
|
|
89
|
+
const center = card({ kind: subject.kind === 'relation' ? 'concept' : subject.kind, id: subject.kind === 'relation' ? relation.from : subject.id }, 'center')
|
|
90
|
+
cardLayer.append(center)
|
|
91
|
+
const cards = shown.map(item => ({ item, el: card(item, 'neighbour') }))
|
|
92
|
+
for (const c of cards) cardLayer.append(c.el)
|
|
93
|
+
const narrow = w < 760
|
|
94
|
+
graphHost.classList.toggle('is-list', narrow)
|
|
95
|
+
const positions = []
|
|
96
|
+
const cw = center.offsetWidth, ch = center.offsetHeight
|
|
97
|
+
const cx = narrow ? w / 2 : w / 2, cy = narrow ? ch / 2 + 12 : hgt / 2
|
|
98
|
+
place(center, cx - cw / 2, cy - ch / 2)
|
|
99
|
+
const rects = new Map([['center', { x: cx - cw / 2, y: cy - ch / 2, w: cw, h: ch }]])
|
|
100
|
+
if (narrow) {
|
|
101
|
+
let y = cy + ch / 2 + 22
|
|
102
|
+
for (const c of cards) { const cw2 = Math.min(w - 24, 340); c.el.style.width = cw2 + 'px'; place(c.el, (w - cw2) / 2, y); rects.set(c.item.id, { x: (w - cw2) / 2, y, w: cw2, h: c.el.offsetHeight }); y += c.el.offsetHeight + 10 }
|
|
103
|
+
graphHost.style.minHeight = y + 'px'
|
|
104
|
+
} else {
|
|
105
|
+
graphHost.style.minHeight = ''
|
|
106
|
+
// Incoming connections on the left, outgoing on the right: the page reads as a flow.
|
|
107
|
+
const columns = { in: cards.filter(c => c.item.dir === 'in'), out: cards.filter(c => c.item.dir !== 'in') }
|
|
108
|
+
const gap = 12, room = hgt - 64
|
|
109
|
+
for (const [side, list] of Object.entries(columns)) {
|
|
110
|
+
let used = 0
|
|
111
|
+
const fits = []
|
|
112
|
+
for (const c of list) { const eh = c.el.offsetHeight; if (used + eh > room) { c.el.remove(); continue } fits.push(c); used += eh + gap }
|
|
113
|
+
columns[side] = fits
|
|
114
|
+
let y = Math.max(18, cy - (used - gap) / 2)
|
|
115
|
+
for (const c of fits) {
|
|
116
|
+
const ew = c.el.offsetWidth, eh = c.el.offsetHeight
|
|
117
|
+
const lane = Math.max(84, Math.min(150, (w - cw - 2 * ew - 28) / 2))
|
|
118
|
+
const x = side === 'in' ? Math.max(14, cx - cw / 2 - lane - ew) : Math.min(w - ew - 14, cx + cw / 2 + lane)
|
|
119
|
+
place(c.el, x, y)
|
|
120
|
+
rects.set(c.item.id, { x, y, w: ew, h: eh })
|
|
121
|
+
y += eh + gap
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
cards.splice(0, cards.length, ...columns.in, ...columns.out)
|
|
125
|
+
const a = rects.get('center')
|
|
126
|
+
for (const c of cards) {
|
|
127
|
+
const b = rects.get(c.item.id)
|
|
128
|
+
const from = c.item.dir === 'out' ? a : b, to = c.item.dir === 'out' ? b : a
|
|
129
|
+
drawEdge(from, to, c.item, c.item.relation)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (total > rects.size - 1) cardLayer.append(h('p', { class: 'wb-more', text: `${total - (rects.size - 1)} more connections are listed in the sheet and in Text view.` }))
|
|
133
|
+
lastLayout = { cards: [...rects.entries()].map(([id, r]) => ({ id, ...r })), shown: rects.size - 1, total, narrow }
|
|
134
|
+
if (refocus) [...cardLayer.querySelectorAll(`.wb-${refocus.role}`)].find(x => x.dataset.kind === refocus.kind && x.dataset.id === refocus.id)?.focus({ preventScroll: true })
|
|
135
|
+
if (animate && subject.from && !reduced()) {
|
|
136
|
+
const r = rects.get('center')
|
|
137
|
+
const dx = subject.from.x - (r.x + r.w / 2) - graphHost.getBoundingClientRect().left, dy = subject.from.y - (r.y + r.h / 2) - graphHost.getBoundingClientRect().top
|
|
138
|
+
center.animate([{ transform: `translate(${dx}px, ${dy}px) scale(0.35)`, opacity: 0.2 }, { transform: 'none', opacity: 1 }], { duration: 420, easing: 'cubic-bezier(0.2,0,0,1)' })
|
|
139
|
+
for (const c of cards) c.el.animate([{ opacity: 0, transform: 'scale(0.92)' }, { opacity: 1, transform: 'none' }], { duration: 360, delay: 140, easing: 'ease-out', fill: 'backwards' })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function place(el, x, y) { el.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)` }
|
|
143
|
+
|
|
144
|
+
function drawEdge(a, b, item, relationId) {
|
|
145
|
+
const ac = { x: a.x + a.w / 2, y: a.y + a.h / 2 }, bc = { x: b.x + b.w / 2, y: b.y + b.h / 2 }
|
|
146
|
+
const clip = (r, from, to) => {
|
|
147
|
+
const dx = to.x - from.x, dy = to.y - from.y
|
|
148
|
+
const tx = dx ? (r.w / 2 + 6) / Math.abs(dx) : Infinity, ty = dy ? (r.h / 2 + 6) / Math.abs(dy) : Infinity
|
|
149
|
+
const t = Math.min(tx, ty)
|
|
150
|
+
return { x: from.x + dx * t, y: from.y + dy * t }
|
|
151
|
+
}
|
|
152
|
+
const start = clip(a, ac, bc), end = clip(b, bc, ac)
|
|
153
|
+
const ns = 'http://www.w3.org/2000/svg'
|
|
154
|
+
const g = document.createElementNS(ns, 'g')
|
|
155
|
+
g.setAttribute('class', `wb-edge ${item.structural ? 'is-declared' : 'is-link'} ${item.predicate === 'contradicts' ? 'is-challenge' : ''} ${item.predicate === 'declares' ? 'is-declares' : ''}`)
|
|
156
|
+
const line = document.createElementNS(ns, 'line')
|
|
157
|
+
for (const [k, v] of Object.entries({ x1: start.x, y1: start.y, x2: end.x, y2: end.y })) line.setAttribute(k, v.toFixed(1))
|
|
158
|
+
const angle = Math.atan2(end.y - start.y, end.x - start.x)
|
|
159
|
+
const head = document.createElementNS(ns, 'path')
|
|
160
|
+
const s = 8
|
|
161
|
+
head.setAttribute('d', `M${end.x},${end.y} L${end.x - Math.cos(angle - 0.42) * s},${end.y - Math.sin(angle - 0.42) * s} L${end.x - Math.cos(angle + 0.42) * s},${end.y - Math.sin(angle + 0.42) * s}Z`)
|
|
162
|
+
g.append(line, head)
|
|
163
|
+
edgeSvg.append(g)
|
|
164
|
+
if (item.structural) {
|
|
165
|
+
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }
|
|
166
|
+
const pill = h('button', { class: 'wb-pill', type: 'button', style: `transform:translate(${Math.round(mid.x)}px, ${Math.round(mid.y)}px) translate(-50%,-50%)` }, verb(item.predicate), h('span', { 'aria-hidden': 'true', class: 'wb-pill-arrow', style: `transform:rotate(${angle}rad)`, text: '→' }))
|
|
167
|
+
pill.setAttribute('aria-label', `${verb(item.predicate)}, direction from ${item.dir === 'out' ? 'this subject' : 'the other card'}`)
|
|
168
|
+
if (relationId) pill.addEventListener('click', () => walk({ kind: 'relation', id: relationId }))
|
|
169
|
+
else pill.tabIndex = -1
|
|
170
|
+
cardLayer.append(pill)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Sheet ──
|
|
175
|
+
function renderSheet() {
|
|
176
|
+
clear(sheet)
|
|
177
|
+
if (subject.kind === 'record') return recordSheet()
|
|
178
|
+
const isConcept = subject.kind === 'concept'
|
|
179
|
+
const item = isConcept ? conceptById.get(subject.id) : plan.relations.find(r => r.id === subject.id)
|
|
180
|
+
const titleOf = id => conceptById.get(id)?.title ?? id
|
|
181
|
+
const steps = isConcept ? conceptSteps(item) : relationSteps(item, titleOf)
|
|
182
|
+
const d = isConcept ? store.concept(item) : store.relation(item)
|
|
183
|
+
const guide = isConcept ? guideConcept(item, d, visible) : guideRelation(d, visible)
|
|
184
|
+
const originals = isConcept ? { definition: item.definition, identityRule: item.identityRule } : { meaning: item.meaning }
|
|
185
|
+
const states = steps.map(s => stepState(d[s.id], guide.fields[s.id], store.deferred(subject.kind, subject.id, s.id), originals[s.id]))
|
|
186
|
+
const countText = list => `${list.filter(x => x === 'answered').length} of ${steps.length} drafted · ${list.filter(x => x === 'plan').length} use the plan wording · ${list.filter(x => x === 'deferred').length} deferred`
|
|
187
|
+
step = Math.min(step, steps.length - 1)
|
|
188
|
+
sheet.append(h('p', { class: 'eyebrow', text: isConcept ? `${vocab.concept} · in your plan` : 'Relation · in your plan' }),
|
|
189
|
+
h('h2', { id: 'wb-title', tabindex: '-1', text: isConcept ? item.title : `${titleOf(item.from)} ${verb(item.predicate)} ${titleOf(item.to)}` }),
|
|
190
|
+
h('p', { class: 'sheet-count', id: 'step-count', text: countText(states) }))
|
|
191
|
+
const rail = h('ol', { class: 'step-rail', 'aria-label': 'Steps' })
|
|
192
|
+
steps.forEach((s, i) => rail.append(h('li', {}, h('button', { class: `step step-${states[i]}`, type: 'button', 'aria-current': i === step ? 'step' : null, dataset: { step: s.id },
|
|
193
|
+
onclick: () => { step = i; renderSheet(); sheet.querySelector('.step-body textarea, .step-body input, .step-body button')?.focus() } },
|
|
194
|
+
h('span', { class: 'step-n', text: states[i] === 'answered' ? '✓' : states[i] === 'plan' ? '•' : String(i + 1) }), h('span', { text: s.label })))))
|
|
195
|
+
sheet.append(rail)
|
|
196
|
+
const s = steps[step]
|
|
197
|
+
const body = h('section', { class: 'step-body', 'aria-labelledby': 'step-prompt' })
|
|
198
|
+
body.append(h('h3', { id: 'step-prompt', text: s.prompt }), h('p', { class: 'step-help', text: s.help }))
|
|
199
|
+
const original = isConcept ? { definition: item.definition, identityRule: item.identityRule }[s.id] : s.id === 'meaning' ? item.meaning : undefined
|
|
200
|
+
if (s.kind === 'evidence') body.append(evidenceStep(item, d))
|
|
201
|
+
else {
|
|
202
|
+
const ta = h('textarea', { id: 'step-input', rows: 5, 'aria-describedby': 'step-guide', oninput: e => { d[s.id] = e.target.value; store.changed(); updateGuide() } }, d[s.id])
|
|
203
|
+
body.append(ta)
|
|
204
|
+
if (original !== undefined && original !== d[s.id]) body.append(h('details', { class: 'plan-wording' }, h('summary', { text: 'Current plan wording' }), h('p', { text: original })))
|
|
205
|
+
}
|
|
206
|
+
const guideList = h('ul', { class: 'guide', id: 'step-guide', 'aria-live': 'polite' })
|
|
207
|
+
const updateGuide = () => {
|
|
208
|
+
const g = isConcept ? guideConcept(item, d, visible) : guideRelation(d, visible)
|
|
209
|
+
clear(guideList)
|
|
210
|
+
for (const m of g.fields[s.id] ?? []) guideList.append(h('li', { class: 'guide-' + m.tone, text: m.text }))
|
|
211
|
+
const now = steps.map(x => stepState(d[x.id], g.fields[x.id], store.deferred(subject.kind, subject.id, x.id), originals[x.id]))
|
|
212
|
+
sheet.querySelector('#step-count').textContent = countText(now)
|
|
213
|
+
const button = rail.querySelectorAll('.step')[step]
|
|
214
|
+
const st = now[step]
|
|
215
|
+
button.className = `step step-${st}`
|
|
216
|
+
button.querySelector('.step-n').textContent = st === 'answered' ? '✓' : st === 'plan' ? '•' : String(step + 1)
|
|
217
|
+
layoutCardsText()
|
|
218
|
+
}
|
|
219
|
+
body.append(guideList)
|
|
220
|
+
body.append(h('label', { class: 'defer' }, h('input', { type: 'checkbox', checked: store.deferred(subject.kind, subject.id, s.id), onchange: e => { store.defer(subject.kind, subject.id, s.id, e.target.checked); renderSheet() } }), ' Defer this step'))
|
|
221
|
+
sheet.append(body)
|
|
222
|
+
updateGuide()
|
|
223
|
+
const last = step === steps.length - 1
|
|
224
|
+
sheet.append(h('div', { class: 'sheet-actions' },
|
|
225
|
+
h('button', { class: 'quiet', type: 'button', disabled: step === 0, onclick: () => { step--; renderSheet(); focusStep() } }, 'Back'),
|
|
226
|
+
last ? h('button', { class: 'primary', type: 'button', id: 'review-proposal', onclick: () => onReview() }, 'Review the proposal')
|
|
227
|
+
: h('button', { class: 'primary', type: 'button', id: 'next-step', onclick: () => { step++; renderSheet(); focusStep() } }, 'Next')))
|
|
228
|
+
sheet.append(h('p', { class: 'sheet-note', text: 'Guidance here is a simulation; Atelier’s ontology guide runs on the host. Completeness is not a verdict on truth.' }))
|
|
229
|
+
}
|
|
230
|
+
function focusStep() { sheet.querySelector('#step-input, .step-body .cite')?.focus() }
|
|
231
|
+
function layoutCardsText() {
|
|
232
|
+
const c = cardLayer.querySelector('.wb-center.is-concept .wb-summary')
|
|
233
|
+
if (c && subject.kind === 'concept') c.textContent = store.conceptDraft(subject.id)?.definition ?? ''
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function evidenceStep(item, d) {
|
|
237
|
+
const isConcept = !Object.hasOwn(item, 'predicate')
|
|
238
|
+
const wrap = h('div', { class: 'evidence' })
|
|
239
|
+
const cited = h('ul', { class: 'cited' })
|
|
240
|
+
for (const b of d.evidence) cited.append(h('li', {}, h('span', { text: visible.has(b.id) ? byId.get(b.id).title : 'A record outside what you can read' }),
|
|
241
|
+
h('button', { class: 'link', type: 'button', onclick: () => { d.evidence = d.evidence.filter(x => x.id !== b.id); store.changed(); renderSheet() } }, 'Remove')))
|
|
242
|
+
wrap.append(h('p', { class: 'label', text: d.evidence.length ? 'Cited' : 'Nothing cited yet' }), cited)
|
|
243
|
+
const cite = id => { if (!byId.has(id)) return; if (!d.evidence.some(b => b.id === id)) { d.evidence.push({ id, title: byId.get(id).title, citedAt: new Date().toISOString() }); store.changed(); renderSheet(); layoutGraph(false) } }
|
|
244
|
+
const suggest = (title, ids, note) => {
|
|
245
|
+
const list = ids.filter(id => byId.has(id) && !d.evidence.some(b => b.id === id)).slice(0, 6)
|
|
246
|
+
if (!list.length) return
|
|
247
|
+
wrap.append(h('p', { class: 'label', text: title }), note ? h('p', { class: 'step-help', text: note }) : null)
|
|
248
|
+
const ul = h('ul', { class: 'suggest' })
|
|
249
|
+
for (const id of list) ul.append(h('li', {}, h('span', { class: 'dot', style: `background:${zoneColor(byId.get(id).zone)}` }), h('span', { class: 'suggest-title', text: byId.get(id).title }), h('button', { class: 'cite secondary small', type: 'button', onclick: () => cite(id) }, 'Cite')))
|
|
250
|
+
wrap.append(ul)
|
|
251
|
+
}
|
|
252
|
+
const Records = vocab.records[0].toUpperCase() + vocab.records.slice(1)
|
|
253
|
+
if (isConcept) {
|
|
254
|
+
suggest(`${Records} that declare this concept`, item.declaredBy, 'The record itself carries this concept’s tag.')
|
|
255
|
+
const term = item.title.toLowerCase()
|
|
256
|
+
const matches = model.nodes.filter(n => !item.declaredBy.includes(n.id) && (n.title + ' ' + n.summary).toLowerCase().includes(term)).map(n => n.id)
|
|
257
|
+
suggest('Text matches', matches, 'These mention the word. That alone does not make them members.')
|
|
258
|
+
} else {
|
|
259
|
+
const ends = [item.from, item.to].map(id => new Set(conceptById.get(id)?.declaredBy ?? []))
|
|
260
|
+
suggest(`${Records} that declare both concepts`, [...ends[0]].filter(id => ends[1].has(id)), 'Each carries both concept tags.')
|
|
261
|
+
suggest(`${Records} that declare either concept`, [...new Set([...ends[0], ...ends[1]])].filter(id => !(ends[0].has(id) && ends[1].has(id))), 'One end only. That alone does not show the relation.')
|
|
262
|
+
}
|
|
263
|
+
const find = h('input', { type: 'search', class: 'find', placeholder: `Find any ${vocab.record}…`, 'aria-label': `Find a ${vocab.record} to cite` })
|
|
264
|
+
const results = h('ul', { class: 'suggest' })
|
|
265
|
+
find.addEventListener('input', () => {
|
|
266
|
+
clear(results)
|
|
267
|
+
const q = find.value.trim().toLowerCase()
|
|
268
|
+
if (q.length < 2) return
|
|
269
|
+
for (const n of model.nodes.filter(n => n.title.toLowerCase().includes(q)).slice(0, 6)) results.append(h('li', {}, h('span', { class: 'dot', style: `background:${zoneColor(n.zone)}` }), h('span', { class: 'suggest-title', text: n.title }), h('button', { class: 'cite secondary small', type: 'button', onclick: () => cite(n.id) }, 'Cite')))
|
|
270
|
+
})
|
|
271
|
+
wrap.append(find, results)
|
|
272
|
+
return wrap
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function recordSheet() {
|
|
276
|
+
const n = byId.get(subject.id)
|
|
277
|
+
const links = adjacency.get(n.id) ?? []
|
|
278
|
+
sheet.append(h('p', { class: 'eyebrow' }, h('i', { class: 'dot', style: `background:${zoneColor(n.zone)}` }), n.zoneLabel),
|
|
279
|
+
h('h2', { id: 'wb-title', tabindex: '-1', text: n.title }),
|
|
280
|
+
h('p', { class: 'lede', text: n.summary }))
|
|
281
|
+
const facts = h('dl', { class: 'facts' })
|
|
282
|
+
for (const [k, v] of [['Kind', n.type], ['Status', n.status], ['Location', n.path], ['Links', `${n.degree} (${n.relationDegree} declared relations)`], ['Declares', n.concepts.length ? n.concepts.map(id => conceptById.get(id)?.title ?? id).join(', ') : 'No concept tags']]) if (v) facts.append(h('dt', { text: k }), h('dd', { text: v }))
|
|
283
|
+
sheet.append(facts)
|
|
284
|
+
if (plan.concepts.length) {
|
|
285
|
+
const select = h('select', { 'aria-label': 'Concept draft to cite into' }, plan.concepts.map(c => h('option', { value: c.id, text: c.title })))
|
|
286
|
+
sheet.append(h('div', { class: 'sheet-actions left' }, select, h('button', { class: 'secondary', type: 'button', id: 'cite-record', onclick: () => {
|
|
287
|
+
const c = conceptById.get(select.value), d = store.concept(c)
|
|
288
|
+
if (!d.evidence.some(b => b.id === n.id)) { d.evidence.push({ id: n.id, title: n.title, citedAt: new Date().toISOString() }); store.changed() }
|
|
289
|
+
walk({ kind: 'concept', id: c.id }); step = conceptSteps(c).findIndex(s => s.kind === 'evidence'); renderSheet()
|
|
290
|
+
} }, 'Cite in this concept’s draft')))
|
|
291
|
+
}
|
|
292
|
+
const list = h('ul', { class: 'link-list' })
|
|
293
|
+
for (const a of links.sort((x, y) => Number((y.edge.kind === 'relation')) - Number((x.edge.kind === 'relation'))).slice(0, 40)) {
|
|
294
|
+
const other = byId.get(a.other)
|
|
295
|
+
list.append(h('li', {}, h('button', { class: 'link-row', type: 'button', onclick: () => walk({ kind: 'record', id: other.id }) },
|
|
296
|
+
h('span', { class: 'dir', 'aria-hidden': 'true', text: a.dir === 'out' ? '→' : '←' }),
|
|
297
|
+
h('span', { text: a.dir === 'out' ? `${verb(a.edge.type)} ${other.title}` : `${other.title} ${verb(a.edge.type)} this` }))))
|
|
298
|
+
}
|
|
299
|
+
sheet.append(h('h3', { class: 'sheet-h', text: `Connections · ${links.length}` }), list)
|
|
300
|
+
if (links.length > 40) sheet.append(h('p', { class: 'step-help', text: `${links.length - 40} more in Text view.` }))
|
|
301
|
+
sheet.append(h('div', { class: 'sheet-actions left' }, h('button', { class: 'quiet', type: 'button', onclick: () => onShowOnMap(n.id) }, 'Show on the constellation')))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function renderHead() {
|
|
305
|
+
clear(head)
|
|
306
|
+
head.append(h('button', { class: 'back', type: 'button', id: 'wb-back', onclick: onBack }, '← Constellation'))
|
|
307
|
+
const crumbs = h('nav', { class: 'trail', 'aria-label': 'Walked so far' })
|
|
308
|
+
trail.forEach((t, i) => {
|
|
309
|
+
if (i) crumbs.append(h('span', { class: 'sep', 'aria-hidden': 'true', text: '›' }))
|
|
310
|
+
const label = t.kind === 'record' ? byId.get(t.id).title : t.kind === 'concept' ? conceptById.get(t.id).title : 'Relation'
|
|
311
|
+
crumbs.append(i === trail.length - 1 ? h('span', { class: 'crumb current', 'aria-current': 'page', text: label }) : h('button', { class: 'crumb', type: 'button', onclick: () => { trail = trail.slice(0, i + 1); subject = { ...t }; step = 0; render(false) } }, label))
|
|
312
|
+
})
|
|
313
|
+
const status = h('span', { class: 'save-status', id: 'save-status', role: 'status' })
|
|
314
|
+
const where = { tab: 'in this tab only', browser: 'in this browser only', host: 'to the workspace host' }[store.persistence] ?? 'in this tab only'
|
|
315
|
+
const paint = st => { status.dataset.state = st; status.textContent = { saving: `Saving ${where}\u2026`, saved: `Saved ${where}`, restored: `Draft restored, saved ${where}`, empty: 'Nothing drafted yet', unavailable: 'Storage unavailable \u2014 the draft lives in this tab' }[st] ?? st }
|
|
316
|
+
paint(store.status)
|
|
317
|
+
unsubscribe?.()
|
|
318
|
+
unsubscribe = store.onStatus(paint)
|
|
319
|
+
head.append(crumbs, status)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function walk(next, fromEl) {
|
|
323
|
+
const r = fromEl?.getBoundingClientRect()
|
|
324
|
+
subject = { ...next, from: r ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null }
|
|
325
|
+
trail = [...trail.filter(t => !(t.kind === next.kind && t.id === next.id)), { kind: next.kind, id: next.id }].slice(-6)
|
|
326
|
+
step = 0
|
|
327
|
+
render(true)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function render(animate) {
|
|
331
|
+
renderHead()
|
|
332
|
+
layoutGraph(animate)
|
|
333
|
+
renderSheet()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
open(next, { from = null } = {}) {
|
|
338
|
+
root.hidden = false
|
|
339
|
+
subject = { ...next, from }
|
|
340
|
+
trail = [{ kind: next.kind, id: next.id }]
|
|
341
|
+
step = 0
|
|
342
|
+
render(true)
|
|
343
|
+
root.querySelector('#wb-title')?.focus()
|
|
344
|
+
},
|
|
345
|
+
close() { root.hidden = true },
|
|
346
|
+
get subject() { return subject && { kind: subject.kind, id: subject.id } },
|
|
347
|
+
get layout() { return lastLayout },
|
|
348
|
+
goToStep(id) { const steps = subject.kind === 'concept' ? conceptSteps(conceptById.get(subject.id)) : relationSteps(plan.relations.find(r => r.id === subject.id), x => x); step = Math.max(0, steps.findIndex(s => s.id === id)); renderSheet() },
|
|
349
|
+
refresh() { if (subject) render(false) },
|
|
350
|
+
}
|
|
351
|
+
}
|