Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
860d5e2
Add styles import/export feature
kmcginnes Jun 26, 2026
8ec4e9b
Add kind and major-version guard to file envelope; adopt in styling a…
kmcginnes Jun 29, 2026
2d72b2f
Add imperative confirm dialog and simplify styling settings flow
kmcginnes Jun 29, 2026
ae61e6e
Simplify file version to a single integer and styling import to whole…
kmcginnes Jun 29, 2026
474028c
Make styling import atomic with whole-file validation
kmcginnes Jun 30, 2026
6e5cd4d
Strip unknown envelope meta fields instead of preserving them
kmcginnes Jun 30, 2026
28d9bf7
Apply styling import review fixes
kmcginnes Jun 30, 2026
0afc1ed
Dispatch import payloads on format generation, failing loudly on a gap
kmcginnes Jun 30, 2026
eb8f4a5
Add golden-file corpus proving every shipped file generation still im…
kmcginnes Jun 30, 2026
ab32f67
Validate envelope version in the schema; split read and write meta co…
kmcginnes Jul 1, 2026
50a9465
Infer ParsedEnvelope from its schema
kmcginnes Jul 1, 2026
013a489
Model empty-import as its own phase; tidy import dialog copy
kmcginnes Jul 1, 2026
f6f5ab2
Fix stale major-version wording in parseExportedGraph docstring
kmcginnes Jul 1, 2026
8d215f6
Drive import dialog with useActionState instead of useMutation
kmcginnes Jul 1, 2026
cb4d545
Cover the dialog dismiss path in ImportStylesButton
kmcginnes Jul 1, 2026
027502d
Rename imported-default styles to shared styles; import/export to loa…
kmcginnes Jul 1, 2026
45c3b9c
Sharpen the save-styles button and setting copy
kmcginnes Jul 1, 2026
b936a33
Note that saved styles include loaded shared styles
kmcginnes Jul 1, 2026
9027019
Sharpen the load-styles button and setting copy
kmcginnes Jul 1, 2026
0843917
Give exported JSON files self-describing names
kmcginnes Jul 1, 2026
4e68fe8
Pre-filter the graph load dialog to .graph.json files
kmcginnes Jul 1, 2026
dc70bd3
Pre-filter the styles load dialog to .styles.json files
kmcginnes Jul 1, 2026
ebd8fb4
Route styling load/save failures through createDisplayError
kmcginnes Jul 1, 2026
d3b7c66
Source import-issue value and message from Zod directly
kmcginnes Jul 1, 2026
187edaa
Match styling file's iconImageType to storage's loose string type
kmcginnes Jul 1, 2026
17f17fb
Update Styles settings docs for load/save and shared-styles naming
kmcginnes Jul 1, 2026
83ca8c2
Restore iconImageType MIME allowlist as a security control
kmcginnes Jul 1, 2026
c897116
Store iconImageType as a loose string, not a MIME enum
kmcginnes Jul 1, 2026
2f8b445
Represent styling test files as the current 3.2.0 build
kmcginnes Jul 1, 2026
40ee3af
End-anchor the lucide icon allowlist branch
kmcginnes Jul 1, 2026
e7880ad
Fix agent-doc drift: shared-layer naming, envelope glossary and kinds
kmcginnes Jul 1, 2026
11e04a9
Wrap styling load/save dispatch in a transition to contain Suspense
kmcginnes Jul 2, 2026
34ce70d
Replace em-dashes in the load-styles conflict dialog
kmcginnes Jul 2, 2026
32450b4
Clarify and standardize the styles settings copy
kmcginnes Jul 2, 2026
478cdc3
Use 'styles' instead of 'styling' in the styles settings copy
kmcginnes Jul 2, 2026
9c0bd02
Scroll only the dialog body, keeping header and footer fixed
kmcginnes Jul 2, 2026
d3daa01
Write graph-export version as '1.0' string for old-build compatibility
kmcginnes Jul 7, 2026
322c0da
Accept any image subtype for icons and enforce it on upload
kmcginnes Jul 7, 2026
7e73065
Rename styling ADR to drop 'salvaging' from the import contract
kmcginnes Jul 7, 2026
d800fcd
Accept application/json in file pickers instead of a filename suffix
kmcginnes Jul 7, 2026
27f071a
Pin the DOMPurify sanitizer contract for custom SVG icons
kmcginnes Jul 7, 2026
be5aae0
Seed shared styles before render in the conflict-prompt test
kmcginnes Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ Correct behavior is necessary but not sufficient — structural quality is a har
- Prefer named function syntax over anonymous arrow functions for module-level declarations (`function handleClick() {}`, not `const handleClick = () => {}`). Arrow functions inside a function body are fine.
- Use an explicit type alias instead of `ReturnType<typeof ...>` when one exists (e.g. `AppStore`, not `ReturnType<typeof getAppStore>`)
- Prefer a branded type over a raw `string`/`number` whenever the value is used for a lookup or passed to a function expecting a value that represents a specific concept — an ID, a node/edge label, a type name, etc. Construct them with their creator function (e.g. `createVertexId()`); never cast a bare string. This makes "which kind of string is this" a compile-time guarantee.
- Prefer Zod at boundaries to enforce contract and strong typing
- Don't change the VS Code setting `typescript.autoClosingTags`
- Prefer throwing upward over local error laundering
- Prefer named domain types over `Record<string, unknown>`
- Do not encode uncertainty as adapters, defaults, optionals, spreads, or catch blocks. Resolve it into the owned contract.

## Git

Expand Down
31 changes: 29 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,22 @@ A key-value pair on a Vertex or Edge. UI label varies by query language: "Proper
_Avoid_: Attribute (legacy code term being phased out)

**Styles**:
Display customizations per Vertex Type and Edge Type — shape, color, icon, line style, and display labels. Persisted and merged with Schema-discovered metadata to produce the final rendering. The two specific instances are **Vertex Styles** and **Edge Styles**, each stored in IndexedDB as its own type-keyed Map: `userVertexStylesAtom` (`Map<VertexType, VertexPreferencesStorageModel>`, key `"user-vertex-styles"`) and `userEdgeStylesAtom` (`Map<EdgeType, EdgePreferencesStorageModel>`, key `"user-edge-styles"`), replacing the former single `userStylingAtom` object. Storage keys follow a `<layer>-<entity>-styles` convention; only the user-defined layer (`user-`) exists today, with additional style layers planned in later work.
_Avoid_: User Preferences, preferences, user styling, user settings (the `*Preferences*` code names — `VertexPreferencesStorageModel`, `userPreferences.ts`, `vertexPreferencesAtom` — are the legacy term being phased out)
Display customizations per Vertex Type and Edge Type — shape, color, icon, line style, and display labels. Resolved through a **Styles Cascade** (highest precedence first): User Custom Styles → Shared Styles → App Default Styles. Persisted and merged with Schema-discovered metadata to produce the final rendering. The two specific instances are **Vertex Styles** and **Edge Styles**, each stored in IndexedDB as its own type-keyed Map per layer. Storage keys follow a `<layer>-<entity>-styles` convention.
_Avoid_: User Preferences, preferences, user styling, user settings (the `*Preferences*` code names — `VertexPreferencesStorageModel`, `userPreferences.ts`, `vertexPreferencesAtom` — are the legacy term being phased out); "customization" as a standalone noun

**Styles Cascade**:
The precedence stack that resolves which style value a vertex or edge type displays. From highest to lowest priority: (3) **User Custom Styles** — per-type edits made in the style dialogs, (2) **Shared Styles** — loaded from a file via Settings, (1) **App Default Styles** — hardcoded in the codebase (`defaultVertexPreferences` / `defaultEdgePreferences`). The layers below User Custom Styles (1–2) collectively are **Default Styles** — what a per-type "Reset to Default" restores to (it clears the user's edit, revealing the Shared or App Default beneath). The verb is **customize**; the UI shorthand is **custom styles** / **shared styles**.
_Avoid_: Imported Default Styles, imported defaults (the layer is **Shared Styles**); Effective styles (no separate atom), style overrides

**User Custom Styles**:
The highest-precedence layer in the Styles Cascade. Per-type edits a user makes in the vertex/edge style dialogs. Stored in `userVertexStylesAtom` (`Map<VertexType, VertexPreferencesStorageModel>`, key `"user-vertex-styles"`) and `userEdgeStylesAtom` (`Map<EdgeType, EdgePreferencesStorageModel>`, key `"user-edge-styles"`). Clearing these ("Reset Custom Styles" in Settings) reveals Shared Styles beneath.

**Shared Styles**:
The second-highest-precedence layer in the Styles Cascade (below User Custom, above App Default). Loaded from a styling file via the Settings → Styles screen. Stored in `sharedVertexStylesAtom` (key `"shared-vertex-styles"`) and `sharedEdgeStylesAtom` (key `"shared-edge-styles"`). Non-destructive to User Custom Styles — loading writes only this layer. Clearing these ("Reset Shared Styles" in Settings) falls through to App Default Styles. Named for their purpose: a user **saves** their styles to a file and others **load** it to get the same look.
_Avoid_: Imported Default Styles, imported defaults (renamed — "shared" names the purpose and reads better)

**App Default Styles**:
The lowest-precedence layer in the Styles Cascade. Hardcoded in the codebase as `defaultVertexPreferences` and `defaultEdgePreferences`. Not persisted — always available as the final fallback.

**Schema Sync**:
The process that queries the database to discover vertex types, edge types, and their attributes. Required before a user can explore a new Connection.
Expand All @@ -78,6 +92,18 @@ _Avoid_: Model, structure
The on-disk JSON format a user gets when they export a Connection (`saveConfigurationToFile`), and which import consumes. It bundles the connection config with a snapshot of the Schema (`lastUpdate` is an ISO string on disk). A single Zod schema in `parseConnectionFile.ts` is the source of truth: both the writer and the importer target the same inferred type (`ExportedConnectionFile`). The writer assigns a `Date` for `lastUpdate` and `JSON.stringify` serializes it to the ISO string; the parser coerces it back via `z.coerce.date()`. The schema is lenient (every level is a `looseObject`), so unknown and legacy fields — styling, `__inferred`/`__matches` on prefixes, attribute `dataType` — pass through untouched. It is intentionally decoupled from the in-memory configuration and from the IndexedDB storage shape, so the wire format can evolve independently. On import it is split — the connection lands in `configurationAtom`, the schema in `schemaAtom`.
_Avoid_: Configuration file (the wire format is not the in-memory or persisted shape)

**File Envelope**:
The shared `{ meta, data }` wrapper for typed JSON export files (`core/fileEnvelope/`). `meta.kind` discriminates the file type and `meta.version` carries the **Format Generation** as a **Wire Version** (see both terms); `parseFileEnvelope` guards both — rejecting a wrong-kind file and one whose version is newer than the build supports — before the caller validates `data` per kind. The write contract (`createFileEnvelope`) also stamps diagnostic `timestamp`/`source`/`sourceVersion`; the read schema keeps only `kind`/`version` and strips the rest (along with any unknown field). Consumers today: **styling-export** and **graph-export**. The **Exported Connection File** is the outlier — it predates the envelope and is a flat shape, not wrapped in one.
_Avoid_: Header, wrapper, metadata block

**Format Generation**:
The integer identifying a File Envelope payload's schema version, bumped **only** on a breaking change (a renamed or removed field); additive changes ship as new optional fields and do not bump it. It is the value `parseFileEnvelope` guards (reject if newer than the build supports) and the consumer's `…ForVersion` switch dispatches on. Distinct from the app version (`sourceVersion`, diagnostic only). Each format owns one named constant for it (`GRAPH_EXPORT_VERSION`, `STYLING_EXPORT_VERSION`).
_Avoid_: Version (unqualified — ambiguous with the app version and the Wire Version)

**Wire Version**:
The version value as literally written in a file's `meta.version`, which normalizes to the **Format Generation** on read (`EnvelopeVersion` = `number | "1.0"`). Usually equals the generation integer, but **graph-export** writes generation 1 as the decimal string `"1.0"` (`GRAPH_EXPORT_WIRE_VERSION`) so builds predating the envelope — which validate `version` as the literal `"1.0"` — can still import files this build writes. **styling-export** has no such installed base and writes the integer `1`. So a reader must not assume `meta.version` is an integer _on disk_ (it always is _after_ normalization).
_Avoid_: Storage version, on-disk version ("storage" means the IndexedDB/persisted shape; "on-disk" already names the whole wire format)

**Persistence Status**:
The single, global state of whether the app's client-side data is safely written to IndexedDB — `idle | saving | failed`. One source of truth that the Persistence Status Indicator subscribes to. `idle` = nothing queued and everything durable (no separate "saved" state — visually identical to a fresh session). `saving` = at least one write queued or in flight, including retryable retries. `failed` = at least one terminal failure outstanding, carrying failure records for a drill-in detail view. Aggregated across all per-key write queues by precedence: any terminal → `failed`; else any in-flight → `saving`; else `idle`. A `failed` key clears on its next successful write; status returns to `idle` when no failure records remain. Deliberately **not** per-key: a failed write flips the whole status rather than naming which collection failed, because the user cannot act on an individual key (the storage layer retries on their behalf). Lives in a plain external store outside React/Jotai; the React edge bridges it via `useSyncExternalStore`.
_Avoid_: Save state (ambiguous with Session)
Expand Down Expand Up @@ -116,6 +142,7 @@ _Avoid_: Save-status indicator

- "Configuration" was used to mean both **Connection** and the bundled object (connection + schema + Styles) — resolved: **Connection** is canonical, "Configuration" is legacy.
- "User Preferences" / "preferences" was used for display customizations, but the code is mid-migration to "styles" (`userVertexStylesAtom` / `userEdgeStylesAtom`, `"user-vertex-styles"` / `"user-edge-styles"`) — resolved: **Styles** is canonical (with **Vertex Styles** / **Edge Styles** as instances); the `*Preferences*` code names are legacy being phased out.
- "Imported Default Styles" / "imported defaults" named the cascade's middle layer, and its file actions were "Import" / "Export" — resolved: the layer is **Shared Styles** ("shared" names the purpose and reads better than a bare adjective-noun), and the actions are **Load** / **Save** to match the house style used by the graph and configuration file features. "Default" now lives only in the cascade generally and the per-type "Reset to Default" button, not the layer name. Renamed end-to-end (atoms `shared*StylesAtom`, keys `"shared-*-styles"`) since the feature had not shipped — no migration. The core-styling plumbing keeps `import`-flavored names (`parseStylingFile`, `useApplyStylingImport`, `getStylingConflicts`) as an internal detail of the load action.
- A Connection's data now has three distinct shapes that look similar but must not be conflated, each with its own explicit type: the **Exported Connection File** (`ExportedConnectionFile`, on-disk wire format), the in-memory merged configuration (`MergedConfiguration` — connection plus a live `Schema` with `lastUpdate` as a `Date`), and the persisted storage shape (`RawConfiguration` — connection only; the schema is stored separately in `schemaAtom`, never embedded). `mergeConfiguration` assembles a `MergedConfiguration` from a stored `RawConfiguration` and its active schema. Remaining work: `configurationAtom` still keys by `RawConfiguration`, to be migrated to a connection record.
- "Node" means **Vertex** in code but is the preferred UI term for property graphs — resolved: use **Vertex** in code, "node" in UI copy.
- "Attribute" vs "Property" — resolved: **Property** is canonical, "attribute" is legacy code term being phased out.
Expand Down
Loading
Loading