@mnstry/atelier 0.2.0-alpha.15 → 0.2.0-alpha.16
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 +4 -0
- package/README.md +1 -1
- package/docs/guides/extensions.md +6 -6
- package/docs/guides/graph-workspace.md +19 -7
- package/docs/guides/upgrade.md +17 -0
- package/docs/reference/graph-workspace.md +4 -4
- package/docs/start/independent-start.md +2 -2
- package/fixtures/templates/local-library.v1.json +2 -1
- package/package.json +1 -1
- package/src/extensions/project.mjs +5 -5
- package/src/graph-workspace/browser/index.mjs +1 -1
- package/src/graph-workspace/browser/{constellation.mjs → map.mjs} +7 -2
- package/src/graph-workspace/browser/styles.mjs +1 -1
- package/src/graph-workspace/browser/workbench.mjs +2 -2
- package/src/graph-workspace/browser/workspace.mjs +53 -51
- package/src/graph-workspace/disclosure.mjs +1 -1
- package/src/graph-workspace/geometry.mjs +1 -1
- package/src/graph-workspace/index.mjs +2 -2
- package/src/graph-workspace/recipes.mjs +24 -12
- package/src/graph-workspace/scale.mjs +5 -2
- package/src/graph-workspace/theme.mjs +2 -2
- package/templates/external-project-workspace/pack-lifecycle.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0-alpha.16
|
|
4
|
+
|
|
5
|
+
- Rename the graph workspace's room from constellation to **map** and its middle layer from coach to **distribution**. The runtime already holds *constellation* as an identity canonical (a grouping of people), and Atelier already defines the distribution tier. New names: `ROOMS` is `map`, `workbench`, `review`; `LAYERS` is `atelier`, `distribution`, `owner`; `planMap`, `createMap`, the shell handle's `map` and `backToMap`, `setBy: 'distribution'`; interface text says "map". The alpha.15 names keep working for this release and are removed in the next: `room: 'constellation'` (listed in the new `DEPRECATED_ROOMS`), `composeRecipes({ coach })` (giving both `coach` and `distribution` refuses), `planConstellation`, `createConstellation`, the handle's `constellation` and `backToConstellation`, and the element's `constellation` class.
|
|
6
|
+
|
|
3
7
|
## 0.2.0-alpha.15
|
|
4
8
|
|
|
5
9
|
- Patch the fast-uri dependency and override from 3.1.8 to 3.1.9, the patched 3.x release for GHSA-w74x-v6f9-j3xg, GHSA-xhq7-xmqj-769h, GHSA-wjr9-j795-jxcr and GHSA-333x-w4r6-6crp (3.1.8 is in each affected range; whether Atelier's use was exploitable is not established). Ajv's own `^3.0.1` range resolves to the same copy, so a bare consumer installs 3.1.9 only, and `npm audit` reports no runtime advisory.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MNSTRY Atelier
|
|
2
2
|
|
|
3
|
-
`0.2.0-alpha.
|
|
3
|
+
`0.2.0-alpha.16` renames the [graph workspace](docs/guides/graph-workspace.md)'s room to the map and its middle layer to distribution, keeping the alpha.15 names for one release. `0.2.0-alpha.15` added the workspace (a map of every readable record, a focused authoring workbench and a review room, built from primitives distributions and owners can compose), renderer-neutral graph views and an [extension SDK and Postgres project workflow](docs/guides/extensions.md). Native host qualification remains a separate gate.
|
|
4
4
|
|
|
5
5
|
**Turn a repository into a living, governed system.**
|
|
6
6
|
|
|
@@ -45,12 +45,12 @@ distributed in this package.
|
|
|
45
45
|
|
|
46
46
|
The runtime parser is required after JSON Schema validation: it enforces unique IDs and cross-references, recovery scope/effect, current-schema readability, Unicode and total JSON bounds. Native Postgres operation results are capped at 60 KiB inside the transaction, leaving room for the original-operation recovery proof within the 64 KiB action output bound. Every failure after successful admission remains an uncertain operation; inspect its durable receipt before deciding what to do next.
|
|
47
47
|
|
|
48
|
-
The
|
|
49
|
-
|
|
50
|
-
the
|
|
51
|
-
|
|
52
|
-
`npm run typecheck` and reseal the reviewed project.
|
|
53
|
-
|
|
48
|
+
The generated project pins the SDK to the Atelier version that generated it.
|
|
49
|
+
When that version is published, `npm install` resolves it. From an unreleased
|
|
50
|
+
checkout, pack the reviewed checkout with `npm pack --ignore-scripts` and install
|
|
51
|
+
the resulting local tarball in the generated project by its absolute path. Then
|
|
52
|
+
run `npm run typecheck` and reseal the reviewed project. An unpublished version
|
|
53
|
+
does not install from the public registry. String length limits use Unicode
|
|
54
54
|
code points, matching JSON Schema; ill-formed surrogate sequences remain refused.
|
|
55
55
|
|
|
56
56
|
The source seal covers package.json, tsconfig.json, src/, present npm lockfiles,
|
|
@@ -3,18 +3,30 @@
|
|
|
3
3
|
A graph workspace lets a person explore a whole knowledge graph, then shape its concepts one step at a time. It is built in three layers, and the last one belongs to the owner:
|
|
4
4
|
|
|
5
5
|
1. **Atelier** ships primitives and defaults: the read model, scale and layout helpers, label placement, recipes, themes, authoring and the browser rooms.
|
|
6
|
-
2. **A
|
|
6
|
+
2. **A distribution** adapts those pieces for a programme or cohort: its own recipes, a theme, perhaps a different shell around the same rooms. This is the same middle tier as an Atelier [distribution](distributions.md): add and rebrand, never alter root semantics.
|
|
7
7
|
3. **The owner** shapes their own system from everything they receive: hiding what they do not use, renaming, adding their own recipes and appearance.
|
|
8
8
|
|
|
9
9
|
Nothing in a layer is hard-coded into the one below it. Recipes and themes are data that compose by name, and every resolved recipe field records which layer set it.
|
|
10
10
|
|
|
11
11
|
## One graph, three rooms
|
|
12
12
|
|
|
13
|
-
- **
|
|
13
|
+
- **Map.** Every readable record, alive on a night-sky canvas (the default), with soft glows for the owner's folders and folder names at a distance. Names fade in by importance as you zoom in and never overlap each other or the controls. Hovering lights a neighbourhood; clicking selects without moving the map. "Let it move" turns on live physics.
|
|
14
14
|
- **Workbench.** A sparse set of rich cards (incoming on the left, outgoing on the right) beside a focused authoring sheet. One subject at a time, one step at a time, with deferral and structural guidance. Drafts belong to one workspace identity (and reader scope, where the host has one), so two workspaces never see each other's drafts. Without an identity they live in this tab only. With `profile.id` or `draftIdentity` they persist in this browser, and with a host storage port they persist there. The interface says which.
|
|
15
15
|
- **Review.** Before and proposed, the state of the evidence, and that a person decides. Nothing is sent.
|
|
16
16
|
|
|
17
|
-
A work **recipe** picks the room, the lens and the single primary action. Lenses (`question`, `concept`, `zone`) belong to the
|
|
17
|
+
A work **recipe** picks the room, the lens and the single primary action. Lenses (`question`, `concept`, `zone`) belong to the map, subjects (`record`, `concept`, `relation`) to the workbench, and the review room takes neither. The primary action is one of `search`, `open-evidence`, `next-step`, `walk`, `review` or `pick`, and only one the recipe's room can carry out (`primaryActionsFor`). Combinations the shell cannot honour are refused when recipes are composed, and recipes the workspace cannot run (a relation recipe with no relations, say) are not offered. Defaults: *Explore*, *Answer a question*, *Refine a concept*, *Inspect a record*. A command bar (⌘K or `/`) replaces persistent navigation. Text view lists every record, and without WebGL every task remains available through Text view and the workbench.
|
|
18
|
+
|
|
19
|
+
## Names from alpha.15
|
|
20
|
+
|
|
21
|
+
0.2.0-alpha.16 renamed the room and the middle layer. The runtime already uses
|
|
22
|
+
*constellation* for a grouping of people, so a view of records is called the
|
|
23
|
+
**map**; the middle layer reuses Atelier's existing **distribution** tier. The
|
|
24
|
+
alpha.15 names keep working for one release and are then removed:
|
|
25
|
+
`room: 'constellation'` is read as `'map'`, `composeRecipes({ coach })` as
|
|
26
|
+
`{ distribution }` (giving both refuses), `planConstellation` and
|
|
27
|
+
`createConstellation` alias `planMap` and `createMap`, and the shell handle's
|
|
28
|
+
`constellation` and `backToConstellation` alias `map` and `backToMap`. The map
|
|
29
|
+
element carries both the `map` and `constellation` classes.
|
|
18
30
|
|
|
19
31
|
## Honesty rules the primitives keep
|
|
20
32
|
|
|
@@ -26,7 +38,7 @@ A work **recipe** picks the room, the lens and the single primary action. Lenses
|
|
|
26
38
|
|
|
27
39
|
## Scale
|
|
28
40
|
|
|
29
|
-
`
|
|
41
|
+
`planMap()` decides whether to draw every record or to show zones. The default budget is 12,000 records or 60,000 links. Above it, `aggregateZones()` gives counts by zone and directed bundles between zones, and `zoneSlice()` opens one zone at a time. Every link touching the zone is accounted for exactly once (drawn, omitted inside the zone, to another zone, or omitted to another zone), and plan concepts list only the kept records that declare them, with the rest counted. Aggregating 30,000 records is part of the test suite. On a laptop GPU the constellation holds 60 fps at 5,000 records, with labels and zone glows.
|
|
30
42
|
|
|
31
43
|
## Build a workspace
|
|
32
44
|
|
|
@@ -43,8 +55,8 @@ import { mountGraphWorkspace } from '@mnstry/atelier/graph-workspace/browser'
|
|
|
43
55
|
const model = buildWorkspaceModel({ graph, plan, zoneRule: { prefix: 'notes/', depth: 1 } })
|
|
44
56
|
layoutWorkspace(model, { Graph, forceAtlas2 }) // or store positions at build time
|
|
45
57
|
|
|
46
|
-
const recipes = composeRecipes({
|
|
47
|
-
const theme = composeTheme(DEFAULT_THEME,
|
|
58
|
+
const recipes = composeRecipes({ distribution: distributionRecipes, owner: ownerRecipes }).recipes
|
|
59
|
+
const theme = composeTheme(DEFAULT_THEME, distributionTheme, ownerTheme) // every layer is checked; one bad layer refuses the theme
|
|
48
60
|
|
|
49
61
|
mountGraphWorkspace(element, { model, recipes, theme, deps: { Sigma, Graph, EdgeArrowProgram, FA2Layout },
|
|
50
62
|
profile: { id: 'workspace-id', name: 'My notebook', vocabulary: { record: 'note', records: 'notes', zone: 'folder' } },
|
|
@@ -57,7 +69,7 @@ Atelier does not depend on `sigma` or `graphology`; the host injects them. Pin s
|
|
|
57
69
|
|
|
58
70
|
## Keeping a shareable layer clean
|
|
59
71
|
|
|
60
|
-
A
|
|
72
|
+
A distribution or owner who builds on private workspaces can check that nothing from them reaches a shareable layer. `fingerprintsFrom()` derives distinctive strings from a private graph and plan (record ids, paths, long titles, summary openings, plan wording and names). `scanForFingerprints()` reports where any of them appears in shareable text, naming the file and a masked sample only.
|
|
61
73
|
|
|
62
74
|
## What this does not do
|
|
63
75
|
|
package/docs/guides/upgrade.md
CHANGED
|
@@ -9,6 +9,23 @@ workspace between Atelier package releases. The flow is local-only: it does not
|
|
|
9
9
|
provision repositories, contact a Git host, mutate the MNSTRY runtime, or write
|
|
10
10
|
through a browser view.
|
|
11
11
|
|
|
12
|
+
## Upgrading to 0.2.0-alpha.16
|
|
13
|
+
|
|
14
|
+
This release renames two graph workspace names; nothing else changes and no
|
|
15
|
+
stored record changes format.
|
|
16
|
+
|
|
17
|
+
- The room `constellation` is now `map`, and the middle recipe and theme layer
|
|
18
|
+
`coach` is now `distribution`. Use `room: 'map'`,
|
|
19
|
+
`composeRecipes({ distribution })`, `planMap`, `createMap`, and the shell
|
|
20
|
+
handle's `map` and `backToMap`. Provenance now reads `setBy: 'distribution'`;
|
|
21
|
+
code that compared `setBy` values with `'coach'` must compare with
|
|
22
|
+
`'distribution'`.
|
|
23
|
+
- The alpha.15 names still work in this release and are removed in the next:
|
|
24
|
+
`room: 'constellation'`, `composeRecipes({ coach })` (do not pass both),
|
|
25
|
+
`planConstellation`, `createConstellation`, `constellation`,
|
|
26
|
+
`backToConstellation` and the `.constellation` class. Styles that target
|
|
27
|
+
`.atelier-gw .constellation` should move to `.atelier-gw .map`.
|
|
28
|
+
|
|
12
29
|
## Upgrading to 0.2.0-alpha.15
|
|
13
30
|
|
|
14
31
|
This release adds the graph workspace (`@mnstry/atelier/graph-workspace` and
|
|
@@ -12,13 +12,13 @@ Two package subpaths. The first is pure and runs anywhere; the second needs a br
|
|
|
12
12
|
| `neighbours(model, id)` | Neighbour ids in either direction. |
|
|
13
13
|
| `seedPositions`, `layoutWorkspace(model, { Graph, forceAtlas2, seed?, iterations?, settings? })`, `normalizePositions`, `LAYOUT_WEIGHTS` | Deterministic zone-seeded ForceAtlas2 layout (injected), normalised to a 1,000-unit frame. |
|
|
14
14
|
| `zoneGeometry(model, { cell? })` | Density blobs and a label anchor per zone. |
|
|
15
|
-
| `
|
|
15
|
+
| `planMap(model, budget?)`, `DEFAULT_BUDGET` | `records` or `zones` mode for the whole view. `planConstellation` is its deprecated alpha.15 name. |
|
|
16
16
|
| `aggregateZones(model)` | `atelier.graph-workspace-zones@v1`: counts by zone and type, directed bundles by edge class, coverage. |
|
|
17
17
|
| `zoneSlice(model, zoneId, { limit })` | Bounded sub-model. `slice` reports `linksTouchingZone` = drawn + `omittedLinksInsideZone` + `linksToOtherZonesCount` + `omittedLinksToOtherZones`, with `linksToOtherZones` per zone. `coverage.byClass` counts the drawn links. Plan concepts keep only kept declaring records, with `omittedDeclarations`. |
|
|
18
18
|
| `rankRecords(model)` | Records by connection count, then title. |
|
|
19
19
|
| `placeLabels({ candidates, measure, width, height, reserved?, budget?, threshold? })` | Collision-free label placement with four slots per label. Pure. |
|
|
20
20
|
| `labelThreshold(ratio, boost?)`, `labelBudget(width, height, ratio, boost?)`, `overlaps` | Defaults for zoom-dependent label density. |
|
|
21
|
-
| `DEFAULT_RECIPES`, `composeRecipes({ atelier?,
|
|
21
|
+
| `DEFAULT_RECIPES`, `composeRecipes({ atelier?, distribution?, owner? })`, `validateRecipe`, `availableRecipes(recipes, model)` | Layered recipes with per-field provenance (`setBy`). Rooms (`ROOMS`): `map`, `workbench`, `review`; layers (`LAYERS`): `atelier`, `distribution`, `owner`. The alpha.15 names `coach` (layer) and `constellation` (room, see `DEPRECATED_ROOMS` and `currentRoom(room)`) are accepted for one release and read as `distribution` and `map`, including in already composed recipes given to `mountGraphWorkspace`; passing both `coach` and `distribution` throws, and only an omitted middle layer defaults (`null` is refused). Lenses (map only): `question`, `concept`, `zone`; subjects (workbench only, required there): `record`, `concept`, `relation`; primary actions (`PRIMARY_ACTIONS`): `search`, `open-evidence`, `next-step`, `walk`, `review`, `pick`, of which `primaryActionsFor(recipe)` names those the shell honours in a recipe's room (review: `review`; workbench: `next-step`, or `walk` for a record; zone lens: `pick`, `search`; question or concept lens: `open-evidence`, `pick`, `search`; no lens: `search`). `composeRecipes` throws on any other combination; `availableRecipes` drops recipes the model cannot run (concept, relation or question work without them in the plan, a review without concepts, a zone lens without zones, a record subject without records). |
|
|
22
22
|
| `DEFAULT_THEME`, `composeTheme(base, ...layers)`, `validateTheme`, `contrastRatio` | `atelier.graph-workspace-theme@v1`, night (dark) by default with a paper (light) scheme. Every layer is checked before and after composing. Each CSS custom property has a declared type (colour, font list, length, shadow, duration, easing), so URLs, `image-set()` and other resource loads are refused, as are unknown fields, schemes and tokens. `--canvas` must equal `graph.canvas`; interface text must reach 4.5:1 on the canvas and surfaces, and on `--surface-glass` composited over the canvas and `--surface-2`; labels need 7:1 and zone colours 3:1. |
|
|
23
23
|
| `conceptSteps`, `relationSteps`, `guideConcept`, `guideRelation`, `stepState`, `readiness(plan, store, readableIds)` | Focused authoring steps and structural guidance. `readiness` needs the `Set` of record ids this reader can open; for concepts and relations alike, missing or unreadable evidence and a missing disconfirming case are errors, so `ready-for-human-review` means every step is complete and every citation is readable here. |
|
|
24
24
|
| `createDraftStore({ storage, key, identity, persistence?, delayMs? })`, `memoryStorage()` | Drafts behind a storage port `{ getItem, setItem, removeItem }`, saved in an `atelier.graph-workspace-draft@v1` envelope bound to `identity` (`{ workspace, ...scope }`); a draft saved under another identity is never restored. `persistence` is `tab`, `browser` or `host`. Status: `empty`, `restored`, `saving`, `saved`, `unavailable`. |
|
|
@@ -28,8 +28,8 @@ Two package subpaths. The first is pure and runs anywhere; the second needs a br
|
|
|
28
28
|
|
|
29
29
|
| Export | Purpose |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| `mountGraphWorkspace(element, { model, deps, recipes?, theme?, scheme?, profile?, storage?, draftIdentity?, persistence?, draftKey?, initialRecipe?, intro?, webgl? })` | Default shell with the three rooms, command bar, display menu, question, concept and zone lenses, the recipe's primary action, selection card and Text view. Drafts stay in the tab unless `draftIdentity` or `profile.id` names the workspace; a host `storage` port requires one. Returns `{ state, recipes, store,
|
|
32
|
-
| `
|
|
31
|
+
| `mountGraphWorkspace(element, { model, deps, recipes?, theme?, scheme?, profile?, storage?, draftIdentity?, persistence?, draftKey?, initialRecipe?, intro?, webgl? })` | Default shell with the three rooms, command bar, display menu, question, concept and zone lenses, the recipe's primary action, selection card and Text view. Drafts stay in the tab unless `draftIdentity` or `profile.id` names the workspace; a host `storage` port requires one. Returns `{ state, recipes, store, map, workbench, select, openWorkbench, openReview, setRecipe, runPrimary, backToMap, toggleText, setScheme, readiness, destroy }`, plus the deprecated aliases `constellation` and `backToConstellation`. One per document. |
|
|
32
|
+
| `createMap({ container, model, palette, deps, font?, reserveSelector?, onSelect?, onHover?, onOpen? })` | The map room alone (`createConstellation` is its deprecated alpha.15 name). `deps = { Sigma, Graph, EdgeArrowProgram, FA2Layout? }`. Elements matching `reserveSelector` (default `[data-graph-reserve]`) are kept free of labels. |
|
|
33
33
|
| `createWorkbench(...)`, `renderReview(...)` | The other two rooms, for custom shells. |
|
|
34
34
|
| `applyTheme(theme, scheme, element)` | Validate and apply a theme scheme as custom properties; returns the graph palette. |
|
|
35
35
|
| `GRAPH_WORKSPACE_CSS`, `injectGraphWorkspaceStyles(document)` | Styles scoped under `.atelier-gw`, responsive by container size. |
|
|
@@ -8,8 +8,8 @@ commands below run locally.
|
|
|
8
8
|
## Find the source and choose a release
|
|
9
9
|
|
|
10
10
|
The maintained public source is [MNSTRY/atelier](https://github.com/MNSTRY/atelier),
|
|
11
|
-
on `main`.
|
|
12
|
-
|
|
11
|
+
on `main`. Its package manifest names the version being prepared; a version in
|
|
12
|
+
source is not a publication receipt.
|
|
13
13
|
The example below pins the published `0.2.0-alpha.14` alpha release. Registry
|
|
14
14
|
`latest` and `alpha` are separate channels; check them before choosing an upgrade:
|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnstry/atelier",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local toolkit that turns a Git repository into an ontology-governed knowledge graph, enforcement layer, and runtime for people, agents, and tools. No telemetry or required hosted account; optional host-bound vault adapters.",
|
|
6
6
|
"keywords": [
|
|
@@ -106,11 +106,11 @@ This project owns its custom data. Atelier generated files only; no account, dat
|
|
|
106
106
|
process, listener, deployment or root installation was created.
|
|
107
107
|
|
|
108
108
|
1. Install the SDK and run npm run typecheck. Keep the generated lockfile.
|
|
109
|
-
The
|
|
110
|
-
|
|
111
|
-
resulting tarball here
|
|
112
|
-
After reviewing that dependency edit, run
|
|
113
|
-
|
|
109
|
+
The SDK is pinned to the Atelier version that generated this project. If that
|
|
110
|
+
version is not on npm yet (an unreleased checkout), run npm pack --ignore-scripts
|
|
111
|
+
in the reviewed Atelier checkout and install the resulting tarball here by its
|
|
112
|
+
absolute path. After reviewing that dependency edit, run
|
|
113
|
+
atelier extension seal --directory .
|
|
114
114
|
2. Select a customer-owned PostgreSQL 17 database. Create a restricted extension_app
|
|
115
115
|
login and apply migrations/001-initial.sql with a separate migration role.
|
|
116
116
|
Keep its DSN in your backend secret store. Do not use the root runtime database.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Browser rooms for the graph workspace. Renderer packages are injected by the host.
|
|
2
2
|
export { mountGraphWorkspace, applyTheme } from './workspace.mjs'
|
|
3
|
-
export { createConstellation } from './
|
|
3
|
+
export { createMap, createConstellation } from './map.mjs'
|
|
4
4
|
export { createWorkbench } from './workbench.mjs'
|
|
5
5
|
export { renderReview } from './review.mjs'
|
|
6
6
|
export { GRAPH_WORKSPACE_CSS, injectGraphWorkspaceStyles } from './styles.mjs'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// deps = { Sigma, Graph, EdgeArrowProgram, FA2Layout? } (sigma 3, graphology,
|
|
3
3
|
// sigma/rendering's EdgeArrowProgram, graphology-layout-forceatlas2/worker).
|
|
4
4
|
//
|
|
5
|
-
// The
|
|
5
|
+
// The map: every readable record, alive. Sigma (WebGL) draws nodes and
|
|
6
6
|
// edges; two canvases around it draw zone glows (behind) and labels (in front).
|
|
7
7
|
// Labels are placed by priority with collision checks, faded by zoom, and drawn
|
|
8
8
|
// with a halo instead of a box. Hovering lights a neighbourhood; selecting keeps it.
|
|
@@ -14,7 +14,7 @@ const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
|
14
14
|
const hexToRgba = (hex, a) => { const n = parseInt(hex.slice(1, 7), 16); return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${a})` }
|
|
15
15
|
const mix = (a, b, t) => { const p = parseInt(a.slice(1, 7), 16), q = parseInt(b.slice(1, 7), 16); const c = s => Math.round(((p >> s) & 255) * (1 - t) + ((q >> s) & 255) * t); return '#' + [16, 8, 0].map(s => c(s).toString(16).padStart(2, '0')).join('') }
|
|
16
16
|
|
|
17
|
-
export function
|
|
17
|
+
export function createMap({ container, model, palette, font = 'system-ui, sans-serif', deps, reserveSelector = '[data-graph-reserve]', onSelect = () => {}, onHover = () => {}, onOpen = () => {} }) {
|
|
18
18
|
const { Sigma, Graph, EdgeArrowProgram, FA2Layout = null } = deps ?? {}
|
|
19
19
|
if (typeof Sigma !== 'function' || typeof Graph !== 'function' || typeof EdgeArrowProgram !== 'function') throw new TypeError('Inject sigma 3 (Sigma, EdgeArrowProgram) and graphology (Graph)')
|
|
20
20
|
if (model.nodes.some(n => !Number.isFinite(n.x) || !Number.isFinite(n.y))) throw new TypeError('Lay the model out first (layoutWorkspace or stored positions)')
|
|
@@ -39,6 +39,8 @@ export function createConstellation({ container, model, palette, font = 'system-
|
|
|
39
39
|
labelCanvas.setAttribute('aria-hidden', 'true')
|
|
40
40
|
const sigmaHost = document.createElement('div')
|
|
41
41
|
sigmaHost.className = 'layer-sigma'
|
|
42
|
+
// The room styles target .map; a container marked only .constellation (alpha.15) gets both.
|
|
43
|
+
container.classList.add('map')
|
|
42
44
|
container.replaceChildren(zoneCanvas, sigmaHost, labelCanvas)
|
|
43
45
|
|
|
44
46
|
// Node sizes are in screen pixels; a small viewport needs proportionally smaller dots.
|
|
@@ -367,3 +369,6 @@ export function createConstellation({ container, model, palette, font = 'system-
|
|
|
367
369
|
}
|
|
368
370
|
return api
|
|
369
371
|
}
|
|
372
|
+
|
|
373
|
+
/** Deprecated alpha.15 name of `createMap`, kept for one release. */
|
|
374
|
+
export const createConstellation = createMap
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Scoped styles for the default graph workspace shell (CSS nesting under .atelier-gw).
|
|
2
2
|
// Colours, type and motion come from the theme's custom properties.
|
|
3
|
-
export const GRAPH_WORKSPACE_CSS = ".atelier-gw{container:atelier-gw / inline-size;position:relative;background:var(--canvas);color:var(--ink);font-family:var(--font-sans);font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased;overflow:hidden;\n/* V6: minimal chrome around a full-bleed constellation; quiet, focused rooms. */\n*{box-sizing:border-box}\n[hidden]{display:none!important}\n\nbutton,input,select,textarea{font:inherit;color:inherit}\nbutton{cursor:pointer;background:none;border:0}\nbutton:disabled{opacity:.45;cursor:default}\n:focus-visible{outline:2px solid var(--focus);outline-offset:2px}\nh2,h3,p{margin:0}\n.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.skip{position:absolute;z-index:100;left:12px;top:10px;transform:translateY(-200%);background:var(--accent);color:var(--accent-ink);padding:8px 12px;border-radius:var(--radius)}\n.skip:focus{transform:none}\n.eyebrow{display:flex;align-items:center;gap:7px;font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin-bottom:6px}\n.label{font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin:14px 0 6px}\n.dot{display:inline-block;width:9px;height:9px;border-radius:50%;flex:none}\n.dot-concept{display:inline-block;width:9px;height:9px;transform:rotate(45deg);background:var(--accent);flex:none;margin-right:2px}\n.primary{background:var(--accent);color:var(--accent-ink);font-weight:600;padding:9px 16px;border-radius:999px;min-height:38px}\n.secondary{border:1px solid var(--line-strong);padding:7px 14px;border-radius:999px;min-height:36px}\n.secondary.small{min-height:30px;padding:3px 11px;font-size:12.5px}\n.quiet{color:var(--ink-2);padding:8px 12px;border-radius:999px;min-height:36px}\n.quiet:hover,.secondary:hover{background:var(--surface-2)}\n.link{color:var(--accent);text-decoration:underline;text-underline-offset:3px;padding:2px 0}\n\n.stage{position:relative;width:100%;height:100%;overflow:hidden}\n.constellation{position:absolute;inset:0}\n.constellation>*{position:absolute;inset:0}\n.layer-zones{z-index:0}\n.layer-sigma{z-index:1}\n.layer-labels{z-index:2;pointer-events:none}\n&.text-mode .constellation,&.text-mode .zoom{visibility:hidden}\n\n/* chrome */\n.top{position:absolute;z-index:10;top:14px;left:14px;right:14px;display:grid;grid-template-columns:1fr minmax(240px,560px) 1fr;gap:10px;align-items:start;pointer-events:none}\n.top>*{pointer-events:auto}\n.top-right{display:flex;gap:8px;justify-self:end}\n.pill{display:inline-flex;align-items:center;gap:8px;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:8px 14px;min-height:38px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);font-weight:500}\n.pill.icon{width:40px;justify-content:center;padding:0;font-size:16px}\n.pill[aria-pressed=true]{background:var(--accent);color:var(--accent-ink);border-color:transparent}\n.emblem{color:var(--accent);font-size:16px}\n.caret{color:var(--ink-3);font-size:11px}\n.menu-wrap{position:relative}\n.menu{position:absolute;top:46px;left:0;z-index:30;min-width:300px;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:6px}\n.top-right .menu{left:auto;right:0}\n.menu-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:9px 12px;border-radius:var(--radius)}\n.menu-item small{color:var(--ink-3);font-size:12px}\n.menu-item:hover,.menu-item[aria-checked=true]{background:var(--surface-2)}\n.menu-item[aria-checked=true] span::after{content:' ✓';color:var(--accent)}\n.menu-label{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);padding:10px 12px 4px}\n.menu-note{font-size:12px;color:var(--ink-3);padding:4px 12px}\n.display .row{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13.5px}\n.display .row.col{flex-direction:column;align-items:stretch;gap:4px}\n.display input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}\n.display input[type=range]{accent-color:var(--accent)}\n.command-wrap{position:relative;justify-self:center;width:100%}\n.command-wrap input{width:100%;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:9px 52px 9px 18px;min-height:40px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}\n.command-wrap input::placeholder{color:var(--ink-3)}\n.command-wrap kbd{position:absolute;right:14px;top:11px;font-size:11px;color:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:0 6px;font-family:inherit}\n.command-results{list-style:none;margin:6px 0 0;padding:6px;position:absolute;left:0;right:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);max-height:60vh;overflow:auto}\n.cmd{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:8px 12px;border-radius:var(--radius)}\n.cmd:hover,.cmd:focus-visible,[aria-selected=true]>.cmd{background:var(--surface-2)}\n.cmd-hint{font-size:12px;color:var(--ink-3)}\n.corner{position:absolute;z-index:5;left:18px;bottom:16px;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--ink-3);max-width:min(520px,60vw);pointer-events:none}\n.corner strong{color:var(--ink-2);font-weight:600;font-size:13px}\n.provenance{color:var(--warn)}\n&[data-private=false] .provenance{color:var(--ink-3)}\n.zoom{position:absolute;z-index:5;right:16px;bottom:16px;display:flex;flex-direction:column;background:var(--surface-glass);border:1px solid var(--line);border-radius:12px;overflow:hidden;backdrop-filter:blur(14px)}\n.zoom button{width:38px;height:36px;font-size:17px;color:var(--ink-2)}\n.zoom button+button{border-top:1px solid var(--line)}\n.zoom button:hover{background:var(--surface-2)}\n.toast{position:absolute;z-index:40;left:50%;top:70px;transform:translateX(-50%);background:var(--surface);border:1px solid var(--line);padding:8px 14px;border-radius:999px;box-shadow:var(--shadow);font-size:13px}\n\n/* selection card */\n.card{position:absolute;z-index:8;left:50%;bottom:18px;transform:translateX(-50%);width:min(560px,calc(100vw - 140px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:16px 18px 14px;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);animation:rise var(--motion) var(--ease)}\n@keyframes rise{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.card h2{font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-.01em;padding-right:28px}\n.card-summary{color:var(--ink-2);margin-top:4px}\n.card-meta{color:var(--ink-3);font-size:12.5px;margin-top:6px}\n.card-actions{display:flex;gap:8px;margin-top:12px;align-items:center}\n.card-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border-radius:50%;color:var(--ink-3);font-size:18px}\n.card-close:hover{background:var(--surface-2)}\n.card-neighbours{margin-top:10px;font-size:13px}\n.card-neighbours summary{cursor:pointer;color:var(--ink-2)}\n.card-neighbours ul{list-style:none;margin:6px 0 0;padding:0;max-height:150px;overflow:auto;columns:2;column-gap:12px}\n.link-row{display:flex;gap:8px;text-align:left;width:100%;padding:4px 6px;border-radius:8px;color:var(--ink-2)}\n.link-row:hover{background:var(--surface-2);color:var(--ink)}\n.dir{color:var(--accent);width:14px;flex:none}\n.question-card{position:absolute;z-index:8;top:66px;left:14px;width:min(360px,calc(100vw - 28px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:14px 16px;backdrop-filter:blur(18px)}\n.question-card select{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;margin-bottom:8px}\n.q-lit{font-weight:600}\n.q-note{font-size:12.5px;color:var(--ink-3);margin:6px 0 10px}\n&.diving .top,&.diving .corner,&.diving .zoom,&.diving .card,&.diving .question-card{opacity:0;transition:opacity 200ms}\n\n/* text view */\n.text-view{position:absolute;inset:0;z-index:3;overflow:auto;padding:76px max(20px,calc(50vw - 520px)) 40px;background:var(--canvas);display:none}\n&.text-mode .text-view{display:block}\n.tv-title{font-family:var(--font-display);font-size:22px;margin-bottom:10px}\n.text-view input[type=search]{width:100%;max-width:420px;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:8px 14px;margin-bottom:10px}\n.text-view h3{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin:14px 0 6px}\n.tv-zone{margin:6px 0;border-top:1px solid var(--line);padding-top:6px}\n.tv-zone summary{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;padding:6px 0}\n.tv-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:2px 12px}\n.tv-item{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;width:100%;text-align:left;padding:6px 8px;border-radius:8px}\n.tv-item small{flex-basis:100%;color:var(--ink-3);font-size:12px}\n.tv-item:hover,.tv-item[aria-pressed=true]{background:var(--surface-2)}\n\n/* workbench */\n.workbench{position:absolute;inset:0;z-index:20;display:grid;grid-template-rows:auto minmax(0,1fr);background:radial-gradient(ellipse at 35% 50%,var(--surface-2),var(--canvas) 70%);animation:fade var(--motion) var(--ease)}\n@keyframes fade{from{opacity:0}to{opacity:1}}\n.wb-head{display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--line);min-height:56px}\n.back{color:var(--ink-2);padding:6px 10px;border-radius:999px}\n.back:hover{background:var(--surface-2)}\n.trail{display:flex;align-items:center;gap:6px;flex:1;min-width:0;overflow:hidden;font-size:13px;color:var(--ink-3)}\n.crumb{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px;color:var(--ink-2)}\nbutton.crumb{text-decoration:underline;text-underline-offset:3px}\n.crumb.current{color:var(--ink);font-weight:600}\n.save-status{font-size:12.5px;color:var(--ink-3);display:flex;align-items:center;gap:6px;white-space:nowrap}\n.save-status::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--ink-3)}\n.save-status[data-state=saved]::before,.save-status[data-state=restored]::before{background:var(--ok)}\n.save-status[data-state=saving]::before{background:var(--warn)}\n.wb-body{display:grid;grid-template-columns:minmax(0,1fr) 440px;grid-template-rows:minmax(0,1fr);min-height:0}\n.wb-graph{position:relative;overflow:hidden}\n.wb-edges{position:absolute;inset:0;width:100%;height:100%;overflow:visible}\n.wb-cards{position:absolute;inset:0}\n.wb-edge line{stroke:var(--line-strong);stroke-width:1.4}\n.wb-edge path{fill:var(--line-strong)}\n.wb-edge.is-declared line{stroke:var(--ink-3);stroke-width:1.6}\n.wb-edge.is-declared path{fill:var(--ink-3)}\n.wb-edge.is-link line{stroke-dasharray:2 5}\n.wb-edge.is-challenge line{stroke:var(--danger);stroke-dasharray:7 4}\n.wb-edge.is-challenge path{fill:var(--danger)}\n.wb-edge.is-declares line{stroke:var(--accent);stroke-dasharray:4 4;opacity:.8}\n.wb-edge.is-declares path{fill:var(--accent)}\n.wb-card{position:absolute;left:0;top:0;width:224px;display:flex;flex-direction:column;align-items:flex-start;gap:3px;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:11px 13px;box-shadow:0 4px 18px rgba(0,0,0,.18);transition:border-color var(--motion)}\n.wb-card:hover{border-color:var(--line-strong)}\n.wb-center{width:280px;border:1.5px solid var(--accent);padding:14px 16px;cursor:default}\n.wb-kind{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-3);letter-spacing:.04em}\n.wb-title{font-weight:600;font-size:14.5px;line-height:1.3}\n.wb-center .wb-title{font-family:var(--font-display);font-size:18px}\n.wb-summary{font-size:12.5px;color:var(--ink-2);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.wb-meta{font-size:11.5px;color:var(--ink-3);display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.tag{font-weight:600;font-size:10.5px;padding:0 6px;border-radius:999px;border:1px solid var(--line-strong)}\n.tag-cited{border-color:var(--ok);color:var(--ok)}\n.tag-proposed{border-color:var(--warn);color:var(--warn);border-style:dashed}\n.wb-pill{position:absolute;left:0;top:0;z-index:2;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;padding:2px 9px;border-radius:999px;background:var(--canvas);border:1px solid var(--line);color:var(--ink-2);white-space:nowrap}\n.wb-pill-arrow{display:inline-block;color:var(--ink)}\n.wb-more{position:absolute;left:16px;bottom:12px;font-size:12px;color:var(--ink-3)}\n.wb-graph.is-list{overflow:auto}\n.wb-graph.is-list .wb-cards{position:relative;min-height:100%}\n.wb-sheet{background:var(--surface);border-left:1px solid var(--line);overflow:auto;padding:22px 26px 30px}\n.wb-sheet h2{font-family:var(--font-display);font-size:24px;font-weight:600;letter-spacing:-.015em;line-height:1.2;margin-bottom:4px}\n.wb-sheet h2:focus{outline:none}\n.sheet-count{font-size:12.5px;color:var(--ink-3)}\n.step-rail{list-style:none;margin:16px 0 18px;padding:0;display:flex;flex-wrap:wrap;gap:6px}\n.step{display:inline-flex;align-items:center;gap:6px;padding:5px 11px 5px 6px;border-radius:999px;border:1px solid var(--line);font-size:12.5px;color:var(--ink-2)}\n.step[aria-current=step]{border-color:var(--accent);color:var(--ink);background:var(--surface-2)}\n.step-n{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;background:var(--surface-2);color:var(--ink-2)}\n.step-answered .step-n{background:var(--ok);color:var(--canvas)}\n.step-deferred .step-n{background:transparent;border:1px dashed var(--ink-3)}\n.step-attention .step-n{background:var(--warn);color:var(--canvas)}\n.step-body h3{font-family:var(--font-display);font-size:20px;font-weight:600;line-height:1.3}\n.step-help{color:var(--ink-3);font-size:13px;margin:4px 0 10px}\n.step-body textarea{width:100%;min-height:150px;resize:vertical;background:var(--canvas);border:1px solid var(--line-strong);border-radius:12px;padding:12px 14px;font-size:16px;line-height:1.55}\n.plan-wording{margin-top:8px;font-size:13px;color:var(--ink-2)}\n.plan-wording summary{cursor:pointer;color:var(--ink-3)}\n.guide{list-style:none;margin:8px 0 0;padding:0;font-size:13px}\n.guide li{padding-left:16px;position:relative;margin-top:3px}\n.guide li::before{position:absolute;left:0;font-weight:700}\n.guide-error{color:var(--danger)}.guide-error::before{content:'!'}\n.guide-warn{color:var(--warn)}.guide-warn::before{content:'•'}\n.guide-info{color:var(--ink-3)}.guide-info::before{content:'i'}\n.defer{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);margin-top:12px}\n.defer input{width:16px;height:16px;accent-color:var(--accent)}\n.sheet-actions{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:20px;padding-top:14px;border-top:1px solid var(--line)}\n.sheet-actions.left{justify-content:flex-start;border-top:0;padding-top:0}\n.sheet-actions select{background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 10px;max-width:180px}\n.sheet-note{font-size:12px;color:var(--ink-3);margin-top:12px}\n.lede{color:var(--ink-2);font-size:15px;margin:6px 0 12px}\n.facts{display:grid;grid-template-columns:96px 1fr;gap:6px 12px;font-size:13px;margin:0 0 16px}\n.facts dt{color:var(--ink-3)}.facts dd{margin:0;word-break:break-word}\n.sheet-h{font-size:13px;margin:12px 0 6px}\n.link-list,.cited,.suggest{list-style:none;margin:0;padding:0}\n.cited li,.suggest li{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}\n.suggest-title,.cited li span{flex:1;min-width:0}\n.find{width:100%;background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 12px;margin-top:12px}\n\n/* review */\n.review-room{position:absolute;inset:0;z-index:30;background:var(--canvas);overflow:auto;animation:fade var(--motion) var(--ease)}\n.review{max-width:860px;margin:0 auto;padding:28px 24px 60px}\n.review-head h2{font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-.02em;margin:12px 0 6px}\n.review-head h2:focus{outline:none}\n.review-status{color:var(--ink-2)}\n.review-status b{color:var(--ink)}\n.review-status[data-status=ready-for-human-review] b{color:var(--ok)}\n.review-status[data-status=needs-attention] b{color:var(--warn)}\n.change{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);padding:18px 20px;margin-top:16px}\n.change h3{font-size:18px;margin-bottom:8px}\n.pair{margin-top:10px}.pair-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}\n.pair small{color:var(--ink-3)}\n.pair blockquote{margin:2px 0 0;padding-left:12px;border-left:3px solid var(--accent)}\n.pair.same small{display:block}\n.line{margin-top:6px}\n.decide{margin-top:22px;padding:16px 20px;border-radius:var(--radius-lg);border:1px solid var(--line-strong)}\n.decide pre{font-size:12px;white-space:pre-wrap;color:var(--ink-2)}\n.decide summary{cursor:pointer;color:var(--ink-3);margin-top:8px}\n\n@container atelier-gw (max-width:860px){\n .wb-body{grid-template-columns:1fr;grid-template-rows:minmax(300px,48dvh) auto;overflow:auto}\n .wb-sheet{border-left:0;border-top:1px solid var(--line)}\n .workbench{overflow:auto}\n}\n@container atelier-gw (max-width:720px){\n .top{grid-template-columns:auto 1fr;top:10px;left:10px;right:10px}\n .command-wrap{grid-column:1/-1;grid-row:2}\n .top-right{grid-column:2;grid-row:1}\n .menu{min-width:min(320px,calc(100vw - 20px))}\n .card{width:calc(100vw - 20px);bottom:10px;padding:14px}\n .card-neighbours ul{columns:1}\n .corner{bottom:auto;top:112px;left:12px;max-width:calc(100vw - 24px)}\n .corner span:not(.provenance){display:none}\n .zoom{bottom:auto;top:104px;right:10px}\n .question-card{top:150px}\n .text-view{padding:120px 14px 30px}\n .wb-head{flex-wrap:wrap;gap:8px}\n .save-status{flex-basis:100%}\n .wb-sheet{padding:18px 16px 30px}\n .pair-grid{grid-template-columns:1fr}\n}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n.step-plan .step-n{background:transparent;border:1px solid var(--accent);color:var(--accent)}\n.top>.menu-wrap{display:flex;gap:8px;align-items:flex-start}\n.primary-action{border-color:var(--accent)}\n\n}\n"
|
|
3
|
+
export const GRAPH_WORKSPACE_CSS = ".atelier-gw{container:atelier-gw / inline-size;position:relative;background:var(--canvas);color:var(--ink);font-family:var(--font-sans);font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased;overflow:hidden;\n/* V6: minimal chrome around a full-bleed map; quiet, focused rooms. */\n*{box-sizing:border-box}\n[hidden]{display:none!important}\n\nbutton,input,select,textarea{font:inherit;color:inherit}\nbutton{cursor:pointer;background:none;border:0}\nbutton:disabled{opacity:.45;cursor:default}\n:focus-visible{outline:2px solid var(--focus);outline-offset:2px}\nh2,h3,p{margin:0}\n.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.skip{position:absolute;z-index:100;left:12px;top:10px;transform:translateY(-200%);background:var(--accent);color:var(--accent-ink);padding:8px 12px;border-radius:var(--radius)}\n.skip:focus{transform:none}\n.eyebrow{display:flex;align-items:center;gap:7px;font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin-bottom:6px}\n.label{font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin:14px 0 6px}\n.dot{display:inline-block;width:9px;height:9px;border-radius:50%;flex:none}\n.dot-concept{display:inline-block;width:9px;height:9px;transform:rotate(45deg);background:var(--accent);flex:none;margin-right:2px}\n.primary{background:var(--accent);color:var(--accent-ink);font-weight:600;padding:9px 16px;border-radius:999px;min-height:38px}\n.secondary{border:1px solid var(--line-strong);padding:7px 14px;border-radius:999px;min-height:36px}\n.secondary.small{min-height:30px;padding:3px 11px;font-size:12.5px}\n.quiet{color:var(--ink-2);padding:8px 12px;border-radius:999px;min-height:36px}\n.quiet:hover,.secondary:hover{background:var(--surface-2)}\n.link{color:var(--accent);text-decoration:underline;text-underline-offset:3px;padding:2px 0}\n\n.stage{position:relative;width:100%;height:100%;overflow:hidden}\n.map{position:absolute;inset:0}\n.map>*{position:absolute;inset:0}\n.layer-zones{z-index:0}\n.layer-sigma{z-index:1}\n.layer-labels{z-index:2;pointer-events:none}\n&.text-mode .map,&.text-mode .zoom{visibility:hidden}\n\n/* chrome */\n.top{position:absolute;z-index:10;top:14px;left:14px;right:14px;display:grid;grid-template-columns:1fr minmax(240px,560px) 1fr;gap:10px;align-items:start;pointer-events:none}\n.top>*{pointer-events:auto}\n.top-right{display:flex;gap:8px;justify-self:end}\n.pill{display:inline-flex;align-items:center;gap:8px;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:8px 14px;min-height:38px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);font-weight:500}\n.pill.icon{width:40px;justify-content:center;padding:0;font-size:16px}\n.pill[aria-pressed=true]{background:var(--accent);color:var(--accent-ink);border-color:transparent}\n.emblem{color:var(--accent);font-size:16px}\n.caret{color:var(--ink-3);font-size:11px}\n.menu-wrap{position:relative}\n.menu{position:absolute;top:46px;left:0;z-index:30;min-width:300px;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:6px}\n.top-right .menu{left:auto;right:0}\n.menu-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:9px 12px;border-radius:var(--radius)}\n.menu-item small{color:var(--ink-3);font-size:12px}\n.menu-item:hover,.menu-item[aria-checked=true]{background:var(--surface-2)}\n.menu-item[aria-checked=true] span::after{content:' ✓';color:var(--accent)}\n.menu-label{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);padding:10px 12px 4px}\n.menu-note{font-size:12px;color:var(--ink-3);padding:4px 12px}\n.display .row{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13.5px}\n.display .row.col{flex-direction:column;align-items:stretch;gap:4px}\n.display input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}\n.display input[type=range]{accent-color:var(--accent)}\n.command-wrap{position:relative;justify-self:center;width:100%}\n.command-wrap input{width:100%;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:9px 52px 9px 18px;min-height:40px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}\n.command-wrap input::placeholder{color:var(--ink-3)}\n.command-wrap kbd{position:absolute;right:14px;top:11px;font-size:11px;color:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:0 6px;font-family:inherit}\n.command-results{list-style:none;margin:6px 0 0;padding:6px;position:absolute;left:0;right:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);max-height:60vh;overflow:auto}\n.cmd{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:8px 12px;border-radius:var(--radius)}\n.cmd:hover,.cmd:focus-visible,[aria-selected=true]>.cmd{background:var(--surface-2)}\n.cmd-hint{font-size:12px;color:var(--ink-3)}\n.corner{position:absolute;z-index:5;left:18px;bottom:16px;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--ink-3);max-width:min(520px,60vw);pointer-events:none}\n.corner strong{color:var(--ink-2);font-weight:600;font-size:13px}\n.provenance{color:var(--warn)}\n&[data-private=false] .provenance{color:var(--ink-3)}\n.zoom{position:absolute;z-index:5;right:16px;bottom:16px;display:flex;flex-direction:column;background:var(--surface-glass);border:1px solid var(--line);border-radius:12px;overflow:hidden;backdrop-filter:blur(14px)}\n.zoom button{width:38px;height:36px;font-size:17px;color:var(--ink-2)}\n.zoom button+button{border-top:1px solid var(--line)}\n.zoom button:hover{background:var(--surface-2)}\n.toast{position:absolute;z-index:40;left:50%;top:70px;transform:translateX(-50%);background:var(--surface);border:1px solid var(--line);padding:8px 14px;border-radius:999px;box-shadow:var(--shadow);font-size:13px}\n\n/* selection card */\n.card{position:absolute;z-index:8;left:50%;bottom:18px;transform:translateX(-50%);width:min(560px,calc(100vw - 140px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:16px 18px 14px;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);animation:rise var(--motion) var(--ease)}\n@keyframes rise{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.card h2{font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-.01em;padding-right:28px}\n.card-summary{color:var(--ink-2);margin-top:4px}\n.card-meta{color:var(--ink-3);font-size:12.5px;margin-top:6px}\n.card-actions{display:flex;gap:8px;margin-top:12px;align-items:center}\n.card-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border-radius:50%;color:var(--ink-3);font-size:18px}\n.card-close:hover{background:var(--surface-2)}\n.card-neighbours{margin-top:10px;font-size:13px}\n.card-neighbours summary{cursor:pointer;color:var(--ink-2)}\n.card-neighbours ul{list-style:none;margin:6px 0 0;padding:0;max-height:150px;overflow:auto;columns:2;column-gap:12px}\n.link-row{display:flex;gap:8px;text-align:left;width:100%;padding:4px 6px;border-radius:8px;color:var(--ink-2)}\n.link-row:hover{background:var(--surface-2);color:var(--ink)}\n.dir{color:var(--accent);width:14px;flex:none}\n.question-card{position:absolute;z-index:8;top:66px;left:14px;width:min(360px,calc(100vw - 28px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:14px 16px;backdrop-filter:blur(18px)}\n.question-card select{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;margin-bottom:8px}\n.q-lit{font-weight:600}\n.q-note{font-size:12.5px;color:var(--ink-3);margin:6px 0 10px}\n&.diving .top,&.diving .corner,&.diving .zoom,&.diving .card,&.diving .question-card{opacity:0;transition:opacity 200ms}\n\n/* text view */\n.text-view{position:absolute;inset:0;z-index:3;overflow:auto;padding:76px max(20px,calc(50vw - 520px)) 40px;background:var(--canvas);display:none}\n&.text-mode .text-view{display:block}\n.tv-title{font-family:var(--font-display);font-size:22px;margin-bottom:10px}\n.text-view input[type=search]{width:100%;max-width:420px;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:8px 14px;margin-bottom:10px}\n.text-view h3{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin:14px 0 6px}\n.tv-zone{margin:6px 0;border-top:1px solid var(--line);padding-top:6px}\n.tv-zone summary{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;padding:6px 0}\n.tv-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:2px 12px}\n.tv-item{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;width:100%;text-align:left;padding:6px 8px;border-radius:8px}\n.tv-item small{flex-basis:100%;color:var(--ink-3);font-size:12px}\n.tv-item:hover,.tv-item[aria-pressed=true]{background:var(--surface-2)}\n\n/* workbench */\n.workbench{position:absolute;inset:0;z-index:20;display:grid;grid-template-rows:auto minmax(0,1fr);background:radial-gradient(ellipse at 35% 50%,var(--surface-2),var(--canvas) 70%);animation:fade var(--motion) var(--ease)}\n@keyframes fade{from{opacity:0}to{opacity:1}}\n.wb-head{display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--line);min-height:56px}\n.back{color:var(--ink-2);padding:6px 10px;border-radius:999px}\n.back:hover{background:var(--surface-2)}\n.trail{display:flex;align-items:center;gap:6px;flex:1;min-width:0;overflow:hidden;font-size:13px;color:var(--ink-3)}\n.crumb{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px;color:var(--ink-2)}\nbutton.crumb{text-decoration:underline;text-underline-offset:3px}\n.crumb.current{color:var(--ink);font-weight:600}\n.save-status{font-size:12.5px;color:var(--ink-3);display:flex;align-items:center;gap:6px;white-space:nowrap}\n.save-status::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--ink-3)}\n.save-status[data-state=saved]::before,.save-status[data-state=restored]::before{background:var(--ok)}\n.save-status[data-state=saving]::before{background:var(--warn)}\n.wb-body{display:grid;grid-template-columns:minmax(0,1fr) 440px;grid-template-rows:minmax(0,1fr);min-height:0}\n.wb-graph{position:relative;overflow:hidden}\n.wb-edges{position:absolute;inset:0;width:100%;height:100%;overflow:visible}\n.wb-cards{position:absolute;inset:0}\n.wb-edge line{stroke:var(--line-strong);stroke-width:1.4}\n.wb-edge path{fill:var(--line-strong)}\n.wb-edge.is-declared line{stroke:var(--ink-3);stroke-width:1.6}\n.wb-edge.is-declared path{fill:var(--ink-3)}\n.wb-edge.is-link line{stroke-dasharray:2 5}\n.wb-edge.is-challenge line{stroke:var(--danger);stroke-dasharray:7 4}\n.wb-edge.is-challenge path{fill:var(--danger)}\n.wb-edge.is-declares line{stroke:var(--accent);stroke-dasharray:4 4;opacity:.8}\n.wb-edge.is-declares path{fill:var(--accent)}\n.wb-card{position:absolute;left:0;top:0;width:224px;display:flex;flex-direction:column;align-items:flex-start;gap:3px;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:11px 13px;box-shadow:0 4px 18px rgba(0,0,0,.18);transition:border-color var(--motion)}\n.wb-card:hover{border-color:var(--line-strong)}\n.wb-center{width:280px;border:1.5px solid var(--accent);padding:14px 16px;cursor:default}\n.wb-kind{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-3);letter-spacing:.04em}\n.wb-title{font-weight:600;font-size:14.5px;line-height:1.3}\n.wb-center .wb-title{font-family:var(--font-display);font-size:18px}\n.wb-summary{font-size:12.5px;color:var(--ink-2);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.wb-meta{font-size:11.5px;color:var(--ink-3);display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.tag{font-weight:600;font-size:10.5px;padding:0 6px;border-radius:999px;border:1px solid var(--line-strong)}\n.tag-cited{border-color:var(--ok);color:var(--ok)}\n.tag-proposed{border-color:var(--warn);color:var(--warn);border-style:dashed}\n.wb-pill{position:absolute;left:0;top:0;z-index:2;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;padding:2px 9px;border-radius:999px;background:var(--canvas);border:1px solid var(--line);color:var(--ink-2);white-space:nowrap}\n.wb-pill-arrow{display:inline-block;color:var(--ink)}\n.wb-more{position:absolute;left:16px;bottom:12px;font-size:12px;color:var(--ink-3)}\n.wb-graph.is-list{overflow:auto}\n.wb-graph.is-list .wb-cards{position:relative;min-height:100%}\n.wb-sheet{background:var(--surface);border-left:1px solid var(--line);overflow:auto;padding:22px 26px 30px}\n.wb-sheet h2{font-family:var(--font-display);font-size:24px;font-weight:600;letter-spacing:-.015em;line-height:1.2;margin-bottom:4px}\n.wb-sheet h2:focus{outline:none}\n.sheet-count{font-size:12.5px;color:var(--ink-3)}\n.step-rail{list-style:none;margin:16px 0 18px;padding:0;display:flex;flex-wrap:wrap;gap:6px}\n.step{display:inline-flex;align-items:center;gap:6px;padding:5px 11px 5px 6px;border-radius:999px;border:1px solid var(--line);font-size:12.5px;color:var(--ink-2)}\n.step[aria-current=step]{border-color:var(--accent);color:var(--ink);background:var(--surface-2)}\n.step-n{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;background:var(--surface-2);color:var(--ink-2)}\n.step-answered .step-n{background:var(--ok);color:var(--canvas)}\n.step-deferred .step-n{background:transparent;border:1px dashed var(--ink-3)}\n.step-attention .step-n{background:var(--warn);color:var(--canvas)}\n.step-body h3{font-family:var(--font-display);font-size:20px;font-weight:600;line-height:1.3}\n.step-help{color:var(--ink-3);font-size:13px;margin:4px 0 10px}\n.step-body textarea{width:100%;min-height:150px;resize:vertical;background:var(--canvas);border:1px solid var(--line-strong);border-radius:12px;padding:12px 14px;font-size:16px;line-height:1.55}\n.plan-wording{margin-top:8px;font-size:13px;color:var(--ink-2)}\n.plan-wording summary{cursor:pointer;color:var(--ink-3)}\n.guide{list-style:none;margin:8px 0 0;padding:0;font-size:13px}\n.guide li{padding-left:16px;position:relative;margin-top:3px}\n.guide li::before{position:absolute;left:0;font-weight:700}\n.guide-error{color:var(--danger)}.guide-error::before{content:'!'}\n.guide-warn{color:var(--warn)}.guide-warn::before{content:'•'}\n.guide-info{color:var(--ink-3)}.guide-info::before{content:'i'}\n.defer{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);margin-top:12px}\n.defer input{width:16px;height:16px;accent-color:var(--accent)}\n.sheet-actions{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:20px;padding-top:14px;border-top:1px solid var(--line)}\n.sheet-actions.left{justify-content:flex-start;border-top:0;padding-top:0}\n.sheet-actions select{background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 10px;max-width:180px}\n.sheet-note{font-size:12px;color:var(--ink-3);margin-top:12px}\n.lede{color:var(--ink-2);font-size:15px;margin:6px 0 12px}\n.facts{display:grid;grid-template-columns:96px 1fr;gap:6px 12px;font-size:13px;margin:0 0 16px}\n.facts dt{color:var(--ink-3)}.facts dd{margin:0;word-break:break-word}\n.sheet-h{font-size:13px;margin:12px 0 6px}\n.link-list,.cited,.suggest{list-style:none;margin:0;padding:0}\n.cited li,.suggest li{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}\n.suggest-title,.cited li span{flex:1;min-width:0}\n.find{width:100%;background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 12px;margin-top:12px}\n\n/* review */\n.review-room{position:absolute;inset:0;z-index:30;background:var(--canvas);overflow:auto;animation:fade var(--motion) var(--ease)}\n.review{max-width:860px;margin:0 auto;padding:28px 24px 60px}\n.review-head h2{font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-.02em;margin:12px 0 6px}\n.review-head h2:focus{outline:none}\n.review-status{color:var(--ink-2)}\n.review-status b{color:var(--ink)}\n.review-status[data-status=ready-for-human-review] b{color:var(--ok)}\n.review-status[data-status=needs-attention] b{color:var(--warn)}\n.change{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);padding:18px 20px;margin-top:16px}\n.change h3{font-size:18px;margin-bottom:8px}\n.pair{margin-top:10px}.pair-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}\n.pair small{color:var(--ink-3)}\n.pair blockquote{margin:2px 0 0;padding-left:12px;border-left:3px solid var(--accent)}\n.pair.same small{display:block}\n.line{margin-top:6px}\n.decide{margin-top:22px;padding:16px 20px;border-radius:var(--radius-lg);border:1px solid var(--line-strong)}\n.decide pre{font-size:12px;white-space:pre-wrap;color:var(--ink-2)}\n.decide summary{cursor:pointer;color:var(--ink-3);margin-top:8px}\n\n@container atelier-gw (max-width:860px){\n .wb-body{grid-template-columns:1fr;grid-template-rows:minmax(300px,48dvh) auto;overflow:auto}\n .wb-sheet{border-left:0;border-top:1px solid var(--line)}\n .workbench{overflow:auto}\n}\n@container atelier-gw (max-width:720px){\n .top{grid-template-columns:auto 1fr;top:10px;left:10px;right:10px}\n .command-wrap{grid-column:1/-1;grid-row:2}\n .top-right{grid-column:2;grid-row:1}\n .menu{min-width:min(320px,calc(100vw - 20px))}\n .card{width:calc(100vw - 20px);bottom:10px;padding:14px}\n .card-neighbours ul{columns:1}\n .corner{bottom:auto;top:112px;left:12px;max-width:calc(100vw - 24px)}\n .corner span:not(.provenance){display:none}\n .zoom{bottom:auto;top:104px;right:10px}\n .question-card{top:150px}\n .text-view{padding:120px 14px 30px}\n .wb-head{flex-wrap:wrap;gap:8px}\n .save-status{flex-basis:100%}\n .wb-sheet{padding:18px 16px 30px}\n .pair-grid{grid-template-columns:1fr}\n}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n.step-plan .step-n{background:transparent;border:1px solid var(--accent);color:var(--accent)}\n.top>.menu-wrap{display:flex;gap:8px;align-items:flex-start}\n.primary-action{border-color:var(--accent)}\n\n}\n"
|
|
4
4
|
|
|
5
5
|
export function injectGraphWorkspaceStyles(doc = document) {
|
|
6
6
|
if (doc.getElementById('atelier-gw-styles')) return
|
|
@@ -298,12 +298,12 @@ export function createWorkbench({ root, model, palette, store, vocab, onBack, on
|
|
|
298
298
|
}
|
|
299
299
|
sheet.append(h('h3', { class: 'sheet-h', text: `Connections · ${links.length}` }), list)
|
|
300
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
|
|
301
|
+
sheet.append(h('div', { class: 'sheet-actions left' }, h('button', { class: 'quiet', type: 'button', onclick: () => onShowOnMap(n.id) }, 'Show on the map')))
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
function renderHead() {
|
|
305
305
|
clear(head)
|
|
306
|
-
head.append(h('button', { class: 'back', type: 'button', id: 'wb-back', onclick: onBack }, '←
|
|
306
|
+
head.append(h('button', { class: 'back', type: 'button', id: 'wb-back', onclick: onBack }, '← Map'))
|
|
307
307
|
const crumbs = h('nav', { class: 'trail', 'aria-label': 'Walked so far' })
|
|
308
308
|
trail.forEach((t, i) => {
|
|
309
309
|
if (i) crumbs.append(h('span', { class: 'sep', 'aria-hidden': 'true', text: '›' }))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Default graph workspace shell: one graph, three rooms (
|
|
2
|
-
// review), chosen by a work recipe.
|
|
1
|
+
// Default graph workspace shell: one graph, three rooms (map, workbench,
|
|
2
|
+
// review), chosen by a work recipe. Distributions can use it as is, restyle it through a
|
|
3
3
|
// theme, change its recipes, or build their own shell from the same rooms.
|
|
4
4
|
//
|
|
5
5
|
// const workspace = mountGraphWorkspace(element, {
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
//
|
|
16
16
|
// One workspace per document (it uses fixed element ids). The host decides what
|
|
17
17
|
// the model contains; nothing here fetches, grants or saves to a source.
|
|
18
|
-
import {
|
|
18
|
+
import { createMap } from './map.mjs'
|
|
19
19
|
import { createWorkbench } from './workbench.mjs'
|
|
20
20
|
import { renderReview } from './review.mjs'
|
|
21
21
|
import { injectGraphWorkspaceStyles } from './styles.mjs'
|
|
22
22
|
import { h, clear } from './dom.mjs'
|
|
23
23
|
import { createDraftStore, readiness, memoryStorage } from '../authoring.mjs'
|
|
24
|
-
import { composeRecipes, availableRecipes } from '../recipes.mjs'
|
|
24
|
+
import { composeRecipes, availableRecipes, currentRoom } from '../recipes.mjs'
|
|
25
25
|
import { DEFAULT_THEME, validateTheme } from '../theme.mjs'
|
|
26
26
|
|
|
27
27
|
const fmt = n => n.toLocaleString('en-US')
|
|
@@ -56,16 +56,18 @@ export function mountGraphWorkspace(root, options) {
|
|
|
56
56
|
model, deps = null, theme = DEFAULT_THEME, profile = {}, initialRecipe = null, intro = true,
|
|
57
57
|
webgl = hasWebGL() && !!deps,
|
|
58
58
|
} = options
|
|
59
|
-
|
|
59
|
+
// Recipes may arrive already composed (possibly by alpha.15 code): read their
|
|
60
|
+
// room names as this release does, keeping everything else, provenance included.
|
|
61
|
+
const recipesAll = (options.recipes ?? composeRecipes().recipes).map(r => (r && typeof r === 'object' && !Array.isArray(r) && 'room' in r ? { ...r, room: currentRoom(r.room) } : r))
|
|
60
62
|
const recipes = availableRecipes(recipesAll, model)
|
|
61
63
|
const byRecipe = new Map(recipes.map(r => [r.id, r]))
|
|
62
64
|
const vocab = { record: 'record', records: 'records', zone: 'zone', concept: 'concept', model: 'Concepts', ...(profile.vocabulary ?? {}) }
|
|
63
65
|
injectGraphWorkspaceStyles(root.ownerDocument)
|
|
64
66
|
|
|
65
67
|
const byNode = new Map(model.nodes.map(n => [n.id, n]))
|
|
66
|
-
const state = { recipe: null, room: '
|
|
68
|
+
const state = { recipe: null, room: 'map', selected: null, lens: null, questionId: null, conceptId: null, zoneId: null, textView: !webgl, scheme: options.scheme ?? theme.defaultScheme, zonesOn: true, linksOn: true, labelBoost: 1, savedCamera: null }
|
|
67
69
|
const counters = { dives: 0 }
|
|
68
|
-
let palette = null,
|
|
70
|
+
let palette = null, mapView = null
|
|
69
71
|
|
|
70
72
|
// ── DOM ──
|
|
71
73
|
const el = {}
|
|
@@ -74,7 +76,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
74
76
|
root.classList.add('atelier-gw')
|
|
75
77
|
const stage = h('div', { class: 'stage' })
|
|
76
78
|
stage.append(
|
|
77
|
-
make('
|
|
79
|
+
make('map', 'div', { class: 'map constellation', role: 'img', 'aria-label': 'Map of every record you can read. Use search, the selection card or Text view to move through it by keyboard.' }),
|
|
78
80
|
h('header', { class: 'top' },
|
|
79
81
|
h('div', { class: 'menu-wrap', 'data-graph-reserve': '' },
|
|
80
82
|
make('recipeBtn', 'button', { class: 'pill', type: 'button', 'aria-haspopup': 'menu', 'aria-expanded': 'false' }, h('span', { class: 'emblem', 'aria-hidden': 'true', text: profile.emblem ?? '◌' }), make('recipeLabel', 'span', { text: '' }), h('span', { class: 'caret', 'aria-hidden': 'true', text: '▾' })),
|
|
@@ -116,7 +118,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
116
118
|
|
|
117
119
|
function applyAppearance() {
|
|
118
120
|
palette = applyTheme(theme, state.scheme, root)
|
|
119
|
-
|
|
121
|
+
mapView?.setPalette(palette)
|
|
120
122
|
el.schemeBtn.textContent = state.scheme === 'dark' ? 'Light appearance' : 'Dark appearance'
|
|
121
123
|
}
|
|
122
124
|
applyAppearance()
|
|
@@ -126,10 +128,10 @@ export function mountGraphWorkspace(root, options) {
|
|
|
126
128
|
|
|
127
129
|
const font = getComputedStyle(root).getPropertyValue('--font-sans').trim() || 'system-ui'
|
|
128
130
|
if (webgl) {
|
|
129
|
-
|
|
130
|
-
if (intro)
|
|
131
|
+
mapView = createMap({ container: el.map, model, palette, font, deps, onSelect: id => select(id), onOpen: id => openWorkbench({ kind: 'record', id }) })
|
|
132
|
+
if (intro) mapView.intro()
|
|
131
133
|
}
|
|
132
|
-
const workbench = createWorkbench({ root: el.workbench, model, palette, store, vocab, onBack:
|
|
134
|
+
const workbench = createWorkbench({ root: el.workbench, model, palette, store, vocab, onBack: backToMap, onReview: () => openReview({ from: 'workbench' }), onShowOnMap: id => backToMap().then(() => { select(id); mapView?.showNode(id) }) })
|
|
133
135
|
el.workbench.hidden = true
|
|
134
136
|
|
|
135
137
|
// ── Recipes ──
|
|
@@ -142,7 +144,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
142
144
|
root.dataset.recipe = recipe.id
|
|
143
145
|
root.dataset.primary = recipe.primary
|
|
144
146
|
closeMenus()
|
|
145
|
-
state.lens = recipe.room === '
|
|
147
|
+
state.lens = recipe.room === 'map' ? recipe.lens : null
|
|
146
148
|
renderLensCard()
|
|
147
149
|
if (recipe.room === 'workbench') {
|
|
148
150
|
// In the workbench the primary action is where focus lands (the top bar sits behind it).
|
|
@@ -152,17 +154,17 @@ export function mountGraphWorkspace(root, options) {
|
|
|
152
154
|
const opened = openWorkbench(subject)
|
|
153
155
|
if (mounted) opened.then(() => focusPrimary(recipe.primary))
|
|
154
156
|
} else if (recipe.room === 'review') openReview({ from: 'recipe' })
|
|
155
|
-
else if (state.room !== '
|
|
157
|
+
else if (state.room !== 'map') backToMap()
|
|
156
158
|
announce(`${recipe.label}. ${recipe.description}`)
|
|
157
159
|
}
|
|
158
160
|
function renderLensCard() {
|
|
159
161
|
el.questionCard.hidden = !state.lens
|
|
160
162
|
el.questionCard.dataset.lens = state.lens ?? ''
|
|
161
|
-
if (state.lens !== 'zone')
|
|
163
|
+
if (state.lens !== 'zone') mapView?.setFocusZone(null)
|
|
162
164
|
if (state.lens === 'question') renderQuestionCard()
|
|
163
165
|
else if (state.lens === 'concept') renderConceptLens()
|
|
164
166
|
else if (state.lens === 'zone') renderZoneLens()
|
|
165
|
-
else { clear(el.questionCard);
|
|
167
|
+
else { clear(el.questionCard); mapView?.setLens(null) }
|
|
166
168
|
}
|
|
167
169
|
function renderConceptLens() {
|
|
168
170
|
clear(el.questionCard)
|
|
@@ -170,7 +172,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
170
172
|
const c = plan.concepts.find(x => x.id === state.conceptId) ?? plan.concepts[0]
|
|
171
173
|
state.conceptId = c.id
|
|
172
174
|
const ids = c.declaredBy.filter(id => byNode.has(id))
|
|
173
|
-
|
|
175
|
+
mapView?.setLens(ids)
|
|
174
176
|
el.questionCard.append(h('p', { class: 'eyebrow', text: `The ${vocab.concept} in view` }),
|
|
175
177
|
h('select', { 'aria-label': vocab.concept, onchange: e => { state.conceptId = e.target.value; renderConceptLens() } }, plan.concepts.map(x => h('option', { value: x.id, selected: x.id === c.id, text: x.title }))),
|
|
176
178
|
h('p', { class: 'q-lit', 'data-gw': 'lens-lit', text: ids.length ? `${fmt(ids.length)} ${ids.length === 1 ? vocab.record : vocab.records} declare it.` : 'No record you can read declares it yet.' }),
|
|
@@ -181,8 +183,8 @@ export function mountGraphWorkspace(root, options) {
|
|
|
181
183
|
const z = model.zones.find(x => x.id === state.zoneId) ?? model.zones[0]
|
|
182
184
|
if (!z) return
|
|
183
185
|
state.zoneId = z.id
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
mapView?.setLens(null)
|
|
187
|
+
mapView?.setFocusZone(z.id)
|
|
186
188
|
el.questionCard.append(h('p', { class: 'eyebrow', text: `The ${vocab.zone} in view` }),
|
|
187
189
|
h('select', { 'aria-label': vocab.zone, onchange: e => { state.zoneId = e.target.value; renderZoneLens() } }, model.zones.map(x => h('option', { value: x.id, selected: x.id === z.id, text: x.label }))),
|
|
188
190
|
h('p', { class: 'q-lit', 'data-gw': 'lens-lit', text: `${fmt(z.count)} ${z.count === 1 ? vocab.record : vocab.records}. Others are dimmed, not removed.` }))
|
|
@@ -219,7 +221,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
219
221
|
const q = plan.questions.find(x => x.id === state.questionId) ?? plan.questions[0]
|
|
220
222
|
state.questionId = q.id
|
|
221
223
|
const ids = new Set(questionLit())
|
|
222
|
-
|
|
224
|
+
mapView?.setLens([...ids])
|
|
223
225
|
const zones = new Set([...ids].map(id => byNode.get(id).zoneLabel))
|
|
224
226
|
el.questionCard.append(h('p', { class: 'eyebrow', text: 'The question on the table' }),
|
|
225
227
|
h('select', { 'aria-label': 'Question', onchange: e => { state.questionId = e.target.value; renderQuestionCard() } }, plan.questions.map(x => h('option', { value: x.id, selected: x.id === q.id, text: x.question }))),
|
|
@@ -231,7 +233,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
231
233
|
// ── Selection and rooms ──
|
|
232
234
|
function select(id) {
|
|
233
235
|
state.selected = id
|
|
234
|
-
|
|
236
|
+
mapView?.select(id)
|
|
235
237
|
renderCard()
|
|
236
238
|
if (id) announce(`Selected ${model.nodes.find(n => n.id === id)?.title}.`)
|
|
237
239
|
}
|
|
@@ -253,20 +255,20 @@ export function mountGraphWorkspace(root, options) {
|
|
|
253
255
|
h('p', { class: 'card-meta', text: `${n.degree} links · ${n.relationDegree} declared${n.concepts.length ? ' · declares ' + n.concepts.join(', ') : ''}` }),
|
|
254
256
|
h('div', { class: 'card-actions' },
|
|
255
257
|
h('button', { class: 'primary', type: 'button', 'data-gw': 'open-selected', onclick: () => openWorkbench({ kind: 'record', id: n.id }) }, 'Open in the workbench'),
|
|
256
|
-
|
|
258
|
+
mapView ? h('button', { class: 'quiet', type: 'button', onclick: () => mapView.showNode(n.id) }, 'Show on map') : null),
|
|
257
259
|
near.length ? h('details', { class: 'card-neighbours' }, h('summary', { text: `Walk to a neighbour (${near.length})` }),
|
|
258
260
|
h('ul', {}, near.slice(0, 12).map(x => h('li', {}, h('button', { class: 'link-row', type: 'button', onclick: () => { select(x.id); root.querySelector('#card-title')?.focus() } }, model.nodes.find(m => m.id === x.id).title))))) : null)
|
|
259
261
|
}
|
|
260
262
|
async function openWorkbench(subject) {
|
|
261
263
|
closeMenus()
|
|
262
264
|
let from = null
|
|
263
|
-
if (state.room === '
|
|
264
|
-
state.savedCamera =
|
|
265
|
+
if (state.room === 'map' && mapView) {
|
|
266
|
+
state.savedCamera = mapView.camera.getState()
|
|
265
267
|
if (subject.kind === 'record') {
|
|
266
|
-
const p =
|
|
268
|
+
const p = mapView.nodeViewport(subject.id), r = el.map.getBoundingClientRect()
|
|
267
269
|
from = p ? { x: r.left + p.x, y: r.top + p.y } : null
|
|
268
270
|
root.classList.add('diving')
|
|
269
|
-
await
|
|
271
|
+
await mapView.dive(subject.id)
|
|
270
272
|
}
|
|
271
273
|
}
|
|
272
274
|
counters.dives++
|
|
@@ -276,13 +278,13 @@ export function mountGraphWorkspace(root, options) {
|
|
|
276
278
|
el.review.hidden = true
|
|
277
279
|
workbench.open(subject, { from })
|
|
278
280
|
}
|
|
279
|
-
async function
|
|
281
|
+
async function backToMap() {
|
|
280
282
|
workbench.close()
|
|
281
283
|
el.review.hidden = true
|
|
282
|
-
state.room = '
|
|
283
|
-
root.dataset.room = '
|
|
284
|
-
if (
|
|
285
|
-
|
|
284
|
+
state.room = 'map'
|
|
285
|
+
root.dataset.room = 'map'
|
|
286
|
+
if (mapView && state.savedCamera) await mapView.restore(state.savedCamera)
|
|
287
|
+
mapView?.refresh()
|
|
286
288
|
}
|
|
287
289
|
function openReview({ from = 'workbench' } = {}) {
|
|
288
290
|
closeMenus()
|
|
@@ -292,8 +294,8 @@ export function mountGraphWorkspace(root, options) {
|
|
|
292
294
|
const toWorkbench = () => { el.review.hidden = true; state.room = 'workbench'; root.dataset.room = 'workbench' }
|
|
293
295
|
renderReview(el.review, {
|
|
294
296
|
model: { ...model, systemId: profile.id ?? 'workspace' }, store,
|
|
295
|
-
backLabel: from === 'workbench' ? '← Back to the workbench' : '← Back to the
|
|
296
|
-
onBack: () => { if (from === 'workbench') { toWorkbench(); workbench.refresh() } else { el.review.hidden = true;
|
|
297
|
+
backLabel: from === 'workbench' ? '← Back to the workbench' : '← Back to the map',
|
|
298
|
+
onBack: () => { if (from === 'workbench') { toWorkbench(); workbench.refresh() } else { el.review.hidden = true; backToMap() } },
|
|
297
299
|
onOpen: s => { toWorkbench(); workbench.open(s) },
|
|
298
300
|
})
|
|
299
301
|
}
|
|
@@ -303,9 +305,9 @@ export function mountGraphWorkspace(root, options) {
|
|
|
303
305
|
const q = query.trim().toLowerCase()
|
|
304
306
|
const commands = [
|
|
305
307
|
...recipes.map(r => ({ kind: 'recipe', id: r.id, label: r.label, hint: r.description })),
|
|
306
|
-
...(webgl ? [{ kind: 'command', id: 'text', label: state.textView ? 'Show the
|
|
308
|
+
...(webgl ? [{ kind: 'command', id: 'text', label: state.textView ? 'Show the map' : 'Text view', hint: 'Every record as a structured list' }] : []),
|
|
307
309
|
{ kind: 'command', id: 'scheme', label: state.scheme === 'dark' ? 'Light appearance' : 'Dark appearance', hint: 'Switch appearance' },
|
|
308
|
-
...(
|
|
310
|
+
...(mapView ? [{ kind: 'command', id: 'fit', label: 'Fit the whole graph', hint: 'Zoom out to everything' }] : []),
|
|
309
311
|
]
|
|
310
312
|
if (!q) return commands.slice(0, 6)
|
|
311
313
|
const out = commands.filter(c => c.label.toLowerCase().includes(q))
|
|
@@ -331,10 +333,10 @@ export function mountGraphWorkspace(root, options) {
|
|
|
331
333
|
else if (r.kind === 'concept') openWorkbench({ kind: 'concept', id: r.id })
|
|
332
334
|
else if (r.kind === 'question') { state.questionId = r.id; const q = recipes.find(x => x.lens === 'question'); if (q) setRecipe(q.id) }
|
|
333
335
|
else if (r.kind === 'zone') { state.zoneId = r.id; const z = recipes.find(x => x.lens === 'zone'); if (z) setRecipe(z.id) }
|
|
334
|
-
else if (r.kind === 'record') { if (state.room !== '
|
|
336
|
+
else if (r.kind === 'record') { if (state.room !== 'map') backToMap().then(() => select(r.id)); else select(r.id); root.querySelector('#card-title')?.focus() }
|
|
335
337
|
else if (r.id === 'text') toggleText()
|
|
336
338
|
else if (r.id === 'scheme') toggleScheme()
|
|
337
|
-
else if (r.id === 'fit')
|
|
339
|
+
else if (r.id === 'fit') mapView?.fit()
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
// ── Text view ──
|
|
@@ -366,7 +368,7 @@ export function mountGraphWorkspace(root, options) {
|
|
|
366
368
|
state.textView = !state.textView
|
|
367
369
|
root.classList.toggle('text-mode', state.textView)
|
|
368
370
|
el.textToggle.setAttribute('aria-pressed', String(state.textView))
|
|
369
|
-
if (state.textView) el.textView.querySelector('input')?.focus(); else
|
|
371
|
+
if (state.textView) el.textView.querySelector('input')?.focus(); else mapView?.refresh()
|
|
370
372
|
}
|
|
371
373
|
function toggleScheme() { state.scheme = state.scheme === 'dark' ? 'light' : 'dark'; applyAppearance(); renderTextView(); renderCard(); if (state.room === 'workbench') workbench.refresh() }
|
|
372
374
|
|
|
@@ -384,13 +386,13 @@ export function mountGraphWorkspace(root, options) {
|
|
|
384
386
|
on(el.displayBtn, 'click', () => toggle(el.display, el.displayBtn))
|
|
385
387
|
on(el.textToggle, 'click', toggleText)
|
|
386
388
|
on(el.schemeBtn, 'click', toggleScheme)
|
|
387
|
-
on(el.zonesToggle, 'change', e => { state.zonesOn = e.target.checked;
|
|
388
|
-
on(el.linksToggle, 'change', e => { state.linksOn = e.target.checked;
|
|
389
|
-
on(el.labelsRange, 'input', e => { state.labelBoost = +e.target.value;
|
|
390
|
-
on(el.physicsToggle, 'change', e => { e.target.checked = !!
|
|
391
|
-
on(el.fit, 'click', () =>
|
|
392
|
-
on(el.zoomIn, 'click', () =>
|
|
393
|
-
on(el.zoomOut, 'click', () =>
|
|
389
|
+
on(el.zonesToggle, 'change', e => { state.zonesOn = e.target.checked; mapView?.setOption('zonesOn', state.zonesOn) })
|
|
390
|
+
on(el.linksToggle, 'change', e => { state.linksOn = e.target.checked; mapView?.setOption('linksOn', state.linksOn) })
|
|
391
|
+
on(el.labelsRange, 'input', e => { state.labelBoost = +e.target.value; mapView?.setOption('labelBoost', state.labelBoost) })
|
|
392
|
+
on(el.physicsToggle, 'change', e => { e.target.checked = !!mapView?.setPhysics(e.target.checked) })
|
|
393
|
+
on(el.fit, 'click', () => mapView?.fit())
|
|
394
|
+
on(el.zoomIn, 'click', () => mapView?.zoom(1.5))
|
|
395
|
+
on(el.zoomOut, 'click', () => mapView?.zoom(1 / 1.5))
|
|
394
396
|
on(el.command, 'focus', renderCommand)
|
|
395
397
|
on(el.command, 'input', renderCommand)
|
|
396
398
|
on(el.command, 'keydown', e => {
|
|
@@ -410,9 +412,9 @@ export function mountGraphWorkspace(root, options) {
|
|
|
410
412
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); el.command.focus(); return }
|
|
411
413
|
if (e.target.closest?.('input, textarea, select') || e.metaKey || e.ctrlKey || e.altKey) return
|
|
412
414
|
if (e.key === '/') { e.preventDefault(); el.command.focus() }
|
|
413
|
-
else if (e.key === 'Escape') { if (!el.recipeMenu.hidden || !el.display.hidden) closeMenus(); else if (state.room === 'review') root.querySelector('#review-back')?.click(); else if (state.room === 'workbench')
|
|
415
|
+
else if (e.key === 'Escape') { if (!el.recipeMenu.hidden || !el.display.hidden) closeMenus(); else if (state.room === 'review') root.querySelector('#review-back')?.click(); else if (state.room === 'workbench') backToMap(); else select(null) }
|
|
414
416
|
else if (e.key === 't') { e.preventDefault(); toggleText() }
|
|
415
|
-
else if (e.key === 'o' && state.selected && state.room === '
|
|
417
|
+
else if (e.key === 'o' && state.selected && state.room === 'map') openWorkbench({ kind: 'record', id: state.selected })
|
|
416
418
|
})
|
|
417
419
|
|
|
418
420
|
let mounted = false
|
|
@@ -421,11 +423,11 @@ export function mountGraphWorkspace(root, options) {
|
|
|
421
423
|
mounted = true
|
|
422
424
|
|
|
423
425
|
return {
|
|
424
|
-
get state() { return { ...state, savedCamera: undefined, counters: { ...counters }, webgl, records: model.nodes.length, links: model.edges.length, workbench: workbench.subject, camera:
|
|
425
|
-
recipes, store, constellation, workbench,
|
|
426
|
-
select, openWorkbench, openReview, setRecipe, runPrimary, backToConstellation, toggleText,
|
|
426
|
+
get state() { return { ...state, savedCamera: undefined, counters: { ...counters }, webgl, records: model.nodes.length, links: model.edges.length, workbench: workbench.subject, camera: mapView ? mapView.camera.getState() : null, draftStatus: store.status, draftPersistence: store.persistence } },
|
|
427
|
+
recipes, store, map: mapView, constellation: mapView, workbench,
|
|
428
|
+
select, openWorkbench, openReview, setRecipe, runPrimary, backToMap, backToConstellation: backToMap, toggleText,
|
|
427
429
|
setScheme(scheme) { state.scheme = scheme; applyAppearance() },
|
|
428
430
|
readiness: () => readiness(model.plan, store, new Set(model.nodes.map(n => n.id))).status,
|
|
429
|
-
destroy() { for (const fn of cleanups.splice(0)) fn();
|
|
431
|
+
destroy() { for (const fn of cleanups.splice(0)) fn(); mapView?.kill(); clear(root); root.classList.remove('atelier-gw') },
|
|
430
432
|
}
|
|
431
433
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Shippable-content gate for layered systems. When a
|
|
1
|
+
// Shippable-content gate for layered systems. When a distribution or owner builds on top of
|
|
2
2
|
// private workspaces, the shareable layer must not carry anything from them.
|
|
3
3
|
// fingerprintsFrom() derives distinctive strings from a private knowledge graph and
|
|
4
4
|
// plan (ids, paths, long titles, summary openings, plan wording, names);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Presentation geometry for the
|
|
1
|
+
// Presentation geometry for the map. Positions are presentation only:
|
|
2
2
|
// distance never asserts a relation. The force layout itself is injected so this
|
|
3
3
|
// package carries no layout dependency; any ForceAtlas2-compatible function works.
|
|
4
4
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// Pure modules only; browser rooms live under ./browser/index.mjs.
|
|
4
4
|
export { buildWorkspaceModel, zoneOf, ROOT_ZONE, validateZoneRule, edgeClass, EDGE_CLASSES, neighbours } from './model.mjs'
|
|
5
5
|
export { seedPositions, layoutWorkspace, normalizePositions, zoneGeometry, LAYOUT_WEIGHTS } from './geometry.mjs'
|
|
6
|
-
export { planConstellation, aggregateZones, zoneSlice, rankRecords, DEFAULT_BUDGET } from './scale.mjs'
|
|
6
|
+
export { planMap, planConstellation, aggregateZones, zoneSlice, rankRecords, DEFAULT_BUDGET } from './scale.mjs'
|
|
7
7
|
export { placeLabels, labelThreshold, labelBudget, overlaps } from './labels.mjs'
|
|
8
|
-
export { DEFAULT_RECIPES, composeRecipes, validateRecipe, availableRecipes, ROOMS, LENSES, SUBJECTS, LAYERS, PRIMARY_ACTIONS, primaryActionsFor } from './recipes.mjs'
|
|
8
|
+
export { DEFAULT_RECIPES, composeRecipes, validateRecipe, availableRecipes, ROOMS, DEPRECATED_ROOMS, currentRoom, LENSES, SUBJECTS, LAYERS, PRIMARY_ACTIONS, primaryActionsFor } from './recipes.mjs'
|
|
9
9
|
export { DEFAULT_THEME, composeTheme, validateTheme, contrastRatio } from './theme.mjs'
|
|
10
10
|
export { conceptSteps, relationSteps, guideConcept, guideRelation, stepState, createDraftStore, memoryStorage, readiness } from './authoring.mjs'
|
|
11
11
|
export { fingerprintsFrom, scanForFingerprints } from './disclosure.mjs'
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
// Work recipes, layered: Atelier ships defaults, a
|
|
2
|
-
//
|
|
1
|
+
// Work recipes, layered: Atelier ships defaults, a distribution (the tier that
|
|
2
|
+
// adapts Atelier for a programme or cohort) adds or adjusts, and the owner has the
|
|
3
|
+
// final say. A recipe is data that chooses the room, the lens
|
|
3
4
|
// and the one primary action. Every resolved field records which layer set it.
|
|
4
5
|
|
|
5
|
-
export const ROOMS = Object.freeze(['
|
|
6
|
+
export const ROOMS = Object.freeze(['map', 'workbench', 'review'])
|
|
7
|
+
/** Names accepted for one release and read as their replacements. */
|
|
8
|
+
export const DEPRECATED_ROOMS = Object.freeze({ constellation: 'map' })
|
|
6
9
|
export const LENSES = Object.freeze([null, 'question', 'concept', 'zone'])
|
|
7
10
|
export const SUBJECTS = Object.freeze([null, 'record', 'concept', 'relation'])
|
|
8
|
-
export const LAYERS = Object.freeze(['atelier', '
|
|
11
|
+
export const LAYERS = Object.freeze(['atelier', 'distribution', 'owner'])
|
|
9
12
|
/** The single emphasised action of a recipe. The default shell implements each one. */
|
|
10
13
|
export const PRIMARY_ACTIONS = Object.freeze(['search', 'open-evidence', 'next-step', 'walk', 'review', 'pick'])
|
|
11
14
|
|
|
@@ -19,14 +22,16 @@ export function primaryActionsFor({ room, lens = null, subject = null }) {
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export const DEFAULT_RECIPES = Object.freeze([
|
|
22
|
-
{ id: 'explore', label: 'Explore', room: '
|
|
23
|
-
{ id: 'question', label: 'Answer a question', room: '
|
|
25
|
+
{ id: 'explore', label: 'Explore', room: 'map', lens: null, subject: null, primary: 'search', description: 'Wander the whole graph. Zones on, search ready.' },
|
|
26
|
+
{ id: 'question', label: 'Answer a question', room: 'map', lens: 'question', subject: null, primary: 'open-evidence', description: 'Light the records that declare a question’s concepts.' },
|
|
24
27
|
{ id: 'refine', label: 'Refine a concept', room: 'workbench', lens: null, subject: 'concept', primary: 'next-step', description: 'One concept, one step at a time.' },
|
|
25
28
|
{ id: 'inspect', label: 'Inspect a record', room: 'workbench', lens: null, subject: 'record', primary: 'walk', description: 'A record and its closest links, with room to read.' },
|
|
26
29
|
].map(Object.freeze))
|
|
27
30
|
|
|
28
31
|
const FIELDS = ['label', 'room', 'lens', 'subject', 'primary', 'description']
|
|
29
32
|
const text = (v, max) => typeof v === 'string' && v.trim().length > 0 && v.length <= max
|
|
33
|
+
/** A room name as this release reads it: deprecated names map to their replacements. */
|
|
34
|
+
export const currentRoom = room => (typeof room === 'string' && Object.hasOwn(DEPRECATED_ROOMS, room) ? DEPRECATED_ROOMS[room] : room)
|
|
30
35
|
|
|
31
36
|
export function validateRecipe(recipe, { partial = false } = {}) {
|
|
32
37
|
const problems = []
|
|
@@ -36,7 +41,7 @@ export function validateRecipe(recipe, { partial = false } = {}) {
|
|
|
36
41
|
const need = !partial
|
|
37
42
|
if ((need || 'label' in recipe) && !text(recipe.label, 60)) problems.push('label must be 1–60 characters')
|
|
38
43
|
if ((need || 'description' in recipe) && !text(recipe.description, 200)) problems.push('description must be 1–200 characters')
|
|
39
|
-
if ((need || 'room' in recipe) && !ROOMS.includes(recipe.room)) problems.push(`room must be one of ${ROOMS.join(', ')}`)
|
|
44
|
+
if ((need || 'room' in recipe) && !ROOMS.includes(currentRoom(recipe.room))) problems.push(`room must be one of ${ROOMS.join(', ')}`)
|
|
40
45
|
if ('lens' in recipe && !LENSES.includes(recipe.lens)) problems.push('unknown lens')
|
|
41
46
|
if ('subject' in recipe && !SUBJECTS.includes(recipe.subject)) problems.push('unknown subject')
|
|
42
47
|
if ((need || 'primary' in recipe) && !PRIMARY_ACTIONS.includes(recipe.primary)) problems.push(`primary must be one of ${PRIMARY_ACTIONS.join(', ')}`)
|
|
@@ -48,22 +53,29 @@ export function validateRecipe(recipe, { partial = false } = {}) {
|
|
|
48
53
|
* Compose recipe layers. Each layer is a list; an entry with an existing id adjusts
|
|
49
54
|
* it (partial), a new id adds a complete recipe, and `hidden: true` removes it from
|
|
50
55
|
* what the owner sees. Later layers win. Throws on any invalid entry.
|
|
56
|
+
* `coach` is the alpha.15 name of the distribution layer, accepted for one release;
|
|
57
|
+
* the room `constellation` is likewise read as `map`.
|
|
51
58
|
*/
|
|
52
|
-
export function composeRecipes({ atelier = DEFAULT_RECIPES, coach
|
|
59
|
+
export function composeRecipes({ atelier = DEFAULT_RECIPES, distribution, coach, owner = [] } = {}) {
|
|
60
|
+
if (distribution !== undefined && coach !== undefined) throw new TypeError('Give the distribution layer once: `coach` is its deprecated name')
|
|
61
|
+
// Only an omitted layer defaults; anything supplied (null included) is validated below.
|
|
62
|
+
const middle = distribution !== undefined ? distribution : coach !== undefined ? coach : []
|
|
53
63
|
const resolved = new Map()
|
|
54
64
|
const provenance = new Map()
|
|
55
|
-
for (const [layer, list] of [['atelier', atelier], ['
|
|
65
|
+
for (const [layer, list] of [['atelier', atelier], ['distribution', middle], ['owner', owner]]) {
|
|
56
66
|
if (!Array.isArray(list)) throw new TypeError(`${layer} recipes must be a list`)
|
|
57
|
-
for (const
|
|
67
|
+
for (const raw of list) {
|
|
68
|
+
// Only plain recipe objects are normalised; anything else reaches validation unchanged.
|
|
69
|
+
const entry = raw && typeof raw === 'object' && !Array.isArray(raw) && 'room' in raw ? { ...raw, room: currentRoom(raw.room) } : raw
|
|
58
70
|
const existing = resolved.get(entry?.id)
|
|
59
71
|
const problems = validateRecipe(entry, { partial: !!existing })
|
|
60
72
|
if (problems.length) throw new TypeError(`${layer} recipe ${entry?.id ?? '(no id)'}: ${problems.join('; ')}`)
|
|
61
73
|
const next = { lens: null, subject: null, hidden: false, ...(existing ?? {}), ...entry }
|
|
62
|
-
// Only combinations a shell can honour: lenses belong to the
|
|
74
|
+
// Only combinations a shell can honour: lenses belong to the map,
|
|
63
75
|
// subjects to the workbench, and the review room takes neither.
|
|
64
76
|
if (next.room === 'workbench' && !next.subject) throw new TypeError(`${layer} recipe ${entry.id}: a workbench recipe needs a subject`)
|
|
65
77
|
if (next.room !== 'workbench' && next.subject) throw new TypeError(`${layer} recipe ${entry.id}: only a workbench recipe takes a subject`)
|
|
66
|
-
if (next.room !== '
|
|
78
|
+
if (next.room !== 'map' && next.lens) throw new TypeError(`${layer} recipe ${entry.id}: only a map recipe takes a lens`)
|
|
67
79
|
const actions = primaryActionsFor(next)
|
|
68
80
|
if (!actions.includes(next.primary)) throw new TypeError(`${layer} recipe ${entry.id}: primary ${next.primary} is not available here; use ${actions.join(' or ')}`)
|
|
69
81
|
resolved.set(entry.id, next)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Scale primitives for workspaces with tens of thousands of records.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// The map draws every readable record while that stays legible and fast.
|
|
4
4
|
// Beyond a budget, the whole is shown as zones (with honest counts and directed
|
|
5
5
|
// bundles between them) and a person opens one zone at a time. Nothing here hides
|
|
6
6
|
// a record silently: every bounded result states what it left out.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const DEFAULT_BUDGET = Object.freeze({ records: 12000, links: 60000 })
|
|
9
9
|
|
|
10
10
|
/** Decide how the whole should be drawn for this model and budget. */
|
|
11
|
-
export function
|
|
11
|
+
export function planMap(model, budget = DEFAULT_BUDGET) {
|
|
12
12
|
const records = model.nodes.length, links = model.edges.length
|
|
13
13
|
if (records <= budget.records && links <= budget.links) return { mode: 'records', records, links }
|
|
14
14
|
return { mode: 'zones', records, links, reason: records > budget.records ? 'records-over-budget' : 'links-over-budget', budget: { ...budget } }
|
|
@@ -93,3 +93,6 @@ export function zoneSlice(model, zoneId, { limit = DEFAULT_BUDGET.records } = {}
|
|
|
93
93
|
},
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/** Deprecated alpha.15 name of `planMap`, kept for one release. */
|
|
98
|
+
export const planConstellation = planMap
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Graph-workspace themes: resolved token data, never stylesheets.
|
|
2
|
-
// Atelier ships two neutral defaults, `night` (the default) and `paper`. A
|
|
2
|
+
// Atelier ships two neutral defaults, `night` (the default) and `paper`. A distribution or
|
|
3
3
|
// owner theme overrides values by name; the result is validated, including text
|
|
4
4
|
// contrast, before any room applies it as CSS custom properties.
|
|
5
5
|
|
|
@@ -126,7 +126,7 @@ export function validateTheme(theme) {
|
|
|
126
126
|
return problems
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/** Override a base theme by layer (
|
|
129
|
+
/** Override a base theme by layer (distribution, then owner). Each layer is validated before it is applied. */
|
|
130
130
|
export function composeTheme(base = DEFAULT_THEME, ...layers) {
|
|
131
131
|
const baseProblems = validateTheme(base)
|
|
132
132
|
if (baseProblems.length) throw new TypeError('Base theme refused: ' + baseProblems.join('; '))
|