Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 4 additions & 14 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,21 @@ _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. 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
_Avoid_: User Preferences, preferences, user styling, user settings; "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**.
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 (`appDefaultVertexStyle` / `appDefaultEdgeStyle`). 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.
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, VertexStyleStorage>`, key `"user-vertex-styles"`) and `userEdgeStylesAtom` (`Map<EdgeType, EdgeStyleStorage>`, 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.
The lowest-precedence layer in the Styles Cascade. Hardcoded in the codebase as `appDefaultVertexStyle` and `appDefaultEdgeStyle`. 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 Down Expand Up @@ -137,13 +137,3 @@ _Avoid_: Save-status indicator

> **Dev:** "Are **Styles** shared across **Connections**?"
> **Domain expert:** "No — well, actually right now they're per **Vertex Type** and **Edge Type** globally, not scoped to a specific **Connection**. So if two databases happen to have the same type name, they'd share styling."

## Flagged ambiguities

- "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.
- "Active connection" meant a single shared per-origin value, but the app consumed it as if it were per-tab — resolved: **Active Connection** is per-tab (sessionStorage), **Last Active Connection** is the shared persisted breadcrumb. The legacy `activeConfigurationAtom` / `active-configuration` key is reused as the breadcrumb.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- **Status:** Accepted
- **Date:** 2026-06-23
- **Updated:** 2026-07-06 — the styling vocabulary was renamed to the Styles convention (#1866). The decision below is unchanged; only the identifiers drifted. Read it with these substitutions: `VertexPreferencesStorageModel`→`VertexStyleStorage`, `EdgePreferencesStorageModel`→`EdgeStyleStorage`, `vertexPreferencesAtom`→`vertexStyleAtom`, `edgePreferencesAtom`→`edgeStyleAtom`, `LegacyUserStylingStorageModel`→`LegacyUserStylingStorage`, `userPreferences.ts`→`graphStyles.ts`.
- **Related:** ADR `per-key-diff-merge-cross-tab-reconciliation` — the Map-keyed shape is the prerequisite for per-type merge. ADR `storage-layer-owns-persistence-failure` — the migration's "catch and degrade to defaults on failure" posture follows that ADR's established startup-failure stance. Issue #1864 (this change). Issues #1820 / #1831 (cross-tab merge, separate follow-up).

## Context
Expand Down
3 changes: 2 additions & 1 deletion docs/adr/20260624-styling-file-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

- **Status:** Accepted
- **Date:** 2026-06-24
- **Related:** ADR `shared-file-envelope` (the outer envelope this payload lives inside). ADR `type-keyed-map-atoms-for-user-preferences` (the Map storage shape that the shared layer mirrors). Issue #1866 (Preferences → Styles rename — deferred, not bundled here).
- **Updated:** 2026-07-06 — the Preferences → Styles rename (#1866) has since shipped. The decision below is unchanged; read `VertexPreferencesStorageModel`→`VertexStyleStorage` and `EdgePreferencesStorageModel`→`EdgeStyleStorage`.
- **Related:** ADR `shared-file-envelope` (the outer envelope this payload lives inside). ADR `type-keyed-map-atoms-for-user-preferences` (the Map storage shape that the shared layer mirrors). Issue #1866 (Preferences → Styles rename).

## Context

Expand Down
2 changes: 1 addition & 1 deletion docs/agents/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ Anything persisted to IndexedDB via localForage/Jotai may be reloaded in an olde

Group them in a dedicated `describe("backward compatibility: ...")` with a comment block stating the old shape, why the tests exist, and a "do not delete without confirming migration" warning. Cast legacy fixtures with `as TypeName` to bypass compile-time checks. See `src/utils/parseConnectionFile.test.ts` for a worked example.

Persisted types that require this when modified: `SchemaStorageModel`, `PrefixTypeConfig`, `VertexTypeConfig`, `EdgeTypeConfig`, `RawConfiguration`, and the user-preference storage models. Triggers: removing/renaming a property, changing a property's type, adding a required property, or changing a property's semantics.
Persisted types that require this when modified: `SchemaStorageModel`, `PrefixTypeConfig`, `VertexTypeConfig`, `EdgeTypeConfig`, `RawConfiguration`, and the style storage models (`VertexStyleStorage`, `EdgeStyleStorage`). Triggers: removing/renaming a property, changing a property's type, adding a required property, or changing a property's semantics.
4 changes: 2 additions & 2 deletions packages/graph-explorer/src/components/Graph/Graph.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export type TextStyle = {
wrap: "none" | "wrap" | "ellipsis";
};

export type NodeStyle = {
export type RenderedNodeStyle = {
background: string;
backgroundFit?: "none" | "contain" | "cover";
backgroundWidth?: string;
Expand Down Expand Up @@ -171,7 +171,7 @@ export type CurveStyle =
| "segments"
| "taxi";

export type EdgeStyle = {
export type RenderedEdgeStyle = {
color: string;
curveStyle: CurveStyle;
label?: string | ((edge: cytoscape.EdgeSingular) => string);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EdgeStyle } from "../Graph.model";
import type { RenderedEdgeStyle } from "../Graph.model";

const defaultEdgeStyle: EdgeStyle = {
const defaultEdgeStyle: RenderedEdgeStyle = {
visible: true,
opacity: 1,
color: "#f0f0f0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NodeStyle } from "../Graph.model";
import type { RenderedNodeStyle } from "../Graph.model";

const defaultNodeStyle: NodeStyle = {
const defaultNodeStyle: RenderedNodeStyle = {
background: "#128EE5",
backgroundOpacity: 0.4,
borderColor: "#128EE5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { EdgeStyle } from "../Graph.model";
import type { RenderedEdgeStyle } from "../Graph.model";

import defaultEdgeStyle from "./defaultEdgeStyle";

const defaultSubwayEdgeStyles: EdgeStyle = {
const defaultSubwayEdgeStyles: RenderedEdgeStyle = {
...defaultEdgeStyle,
curveStyle: "taxi",
taxiDirection: "rightward",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EdgeStyle } from "../Graph.model";
import type { RenderedEdgeStyle } from "../Graph.model";

const hiddenEdgeStyle: Partial<EdgeStyle> = {
const hiddenEdgeStyle: Partial<RenderedEdgeStyle> = {
visible: false,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NodeStyle } from "../Graph.model";
import type { RenderedNodeStyle } from "../Graph.model";

const hiddenNodeStyle: Partial<NodeStyle> = {
const hiddenNodeStyle: Partial<RenderedNodeStyle> = {
visible: false,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EdgeStyle } from "../Graph.model";
import type { RenderedEdgeStyle } from "../Graph.model";

const outOfFocusEdgeStyle: Partial<EdgeStyle> = {
const outOfFocusEdgeStyle: Partial<RenderedEdgeStyle> = {
label: "",
lineColor: "#a0a0a0",
opacity: 0.2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NodeStyle } from "../Graph.model";
import type { RenderedNodeStyle } from "../Graph.model";

const outOfFocusNodeStyle: Partial<NodeStyle> = {
const outOfFocusNodeStyle: Partial<RenderedNodeStyle> = {
backgroundOpacity: 0.1,
background: "#a0a0a0",
borderOpacity: 0.2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EdgeStyle } from "../Graph.model";
import type { RenderedEdgeStyle } from "../Graph.model";

const selectedEdgeStyle: Partial<EdgeStyle> = {
const selectedEdgeStyle: Partial<RenderedEdgeStyle> = {
underlayColor: "#17457b",
underlayOpacity: 0.4,
underlayPadding: 4,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NodeStyle } from "../Graph.model";
import type { RenderedNodeStyle } from "../Graph.model";

const selectedNodeStyle: Partial<NodeStyle> = {
const selectedNodeStyle: Partial<RenderedNodeStyle> = {
underlayColor: "#17457b",
underlayOpacity: 0.4,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { EdgeStyle } from "../Graph.model";
import type { RenderedEdgeStyle } from "../Graph.model";

import selectedEdgeStyle from "./selectedEdgeStyle";

const selectedSubwayEdgeStyles: Partial<EdgeStyle> = {
const selectedSubwayEdgeStyles: Partial<RenderedEdgeStyle> = {
...selectedEdgeStyle,
curveStyle: "taxi",
taxiDirection: "rightward",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { CyEdgeStyle, EdgeStyle } from "../Graph.model";
import type { CyEdgeStyle, RenderedEdgeStyle } from "../Graph.model";

const toCyEdgeStyle = (edgeStyle: Partial<EdgeStyle>): Partial<CyEdgeStyle> => {
const toCyEdgeStyle = (
edgeStyle: Partial<RenderedEdgeStyle>,
): Partial<CyEdgeStyle> => {
return {
color: edgeStyle.text?.color,
curveStyle: edgeStyle.curveStyle,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { CyNodeStyle, NodeStyle } from "../Graph.model";
import type { CyNodeStyle, RenderedNodeStyle } from "../Graph.model";

const toCyNodeStyle = (nodeStyle: Partial<NodeStyle>): Partial<CyNodeStyle> => {
const toCyNodeStyle = (
nodeStyle: Partial<RenderedNodeStyle>,
): Partial<CyNodeStyle> => {
return {
backgroundColor: nodeStyle.background,
backgroundFit: nodeStyle.backgroundFit,
Expand Down
18 changes: 7 additions & 11 deletions packages/graph-explorer/src/components/VertexIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import DOMPurify from "dompurify";
import { DynamicIcon } from "lucide-react/dynamic";
import SVG from "react-inlinesvg";

import {
useVertexPreferences,
type VertexPreferences,
type VertexType,
} from "@/core";
import { useVertexStyle, type VertexStyle, type VertexType } from "@/core";
import { cn } from "@/utils";
import { getLucideName, isValidLucideIconName } from "@/utils/lucideIcons";

Expand All @@ -21,7 +17,7 @@ function sanitizeSvg(svg: string): string {
}

interface Props {
vertexStyle: VertexPreferences;
vertexStyle: VertexStyle;
className?: string;
alt?: string;
}
Expand Down Expand Up @@ -75,15 +71,15 @@ export function VertexIconByType({
vertexType: VertexType;
className?: string;
}) {
const vertexPreferences = useVertexPreferences(vertexType);
return <VertexIcon vertexStyle={vertexPreferences} className={className} />;
const vertexStyle = useVertexStyle(vertexType);
return <VertexIcon vertexStyle={vertexStyle} className={className} />;
}

export function VertexSymbol({
vertexStyle,
className,
}: {
vertexStyle: VertexPreferences;
vertexStyle: VertexStyle;
className?: string;
}) {
return (
Expand All @@ -108,8 +104,8 @@ export function VertexSymbolByType({
vertexType: VertexType;
className?: string;
}) {
const vertexPreferences = useVertexPreferences(vertexType);
return <VertexSymbol vertexStyle={vertexPreferences} className={className} />;
const vertexStyle = useVertexStyle(vertexType);
return <VertexSymbol vertexStyle={vertexStyle} className={className} />;
}

export default VertexIcon;
6 changes: 3 additions & 3 deletions packages/graph-explorer/src/components/VertexRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ComponentPropsWithoutRef } from "react";

import { type DisplayVertex, useVertexPreferences } from "@/core";
import { type DisplayVertex, useVertexStyle } from "@/core";
import { ASCII, cn, LABELS } from "@/utils";

import { SearchResultSubtitle, SearchResultTitle, VertexSymbol } from ".";
Expand All @@ -14,7 +14,7 @@ export function VertexRow({
vertex: DisplayVertex;
name?: string;
} & ComponentPropsWithoutRef<"div">) {
const vertexPreferences = useVertexPreferences(vertex.primaryType);
const vertexStyle = useVertexStyle(vertex.primaryType);
const resultName = name ? `${name}: ` : "";
const nameIsSameAsTypes = vertex.displayTypes === vertex.displayName;
const isDefaultType = vertex.displayTypes === LABELS.MISSING_TYPE;
Expand All @@ -29,7 +29,7 @@ export function VertexRow({
className={cn("flex flex-row items-center gap-3", className)}
{...props}
>
<VertexSymbol vertexStyle={vertexPreferences} />
<VertexSymbol vertexStyle={vertexStyle} />
<div>
<SearchResultTitle>{title}</SearchResultTitle>
<SearchResultSubtitle>{vertex.displayDescription}</SearchResultSubtitle>
Expand Down
10 changes: 5 additions & 5 deletions packages/graph-explorer/src/core/ConfigurationProvider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { ConnectionConfig } from "@shared/types";
import { v4 } from "uuid";

import type {
EdgePreferencesStorageModel,
VertexPreferencesStorageModel,
} from "@/core/StateProvider/userPreferences";
EdgeStyleStorage,
VertexStyleStorage,
} from "@/core/StateProvider/graphStyles";
import type { Branded } from "@/utils";
import type { IriNamespace, RdfPrefix } from "@/utils/rdf";

Expand Down Expand Up @@ -66,7 +66,7 @@ export type VertexTypeConfig = {
* Total number of vertices of this type
*/
total?: number;
} & VertexPreferencesStorageModel;
} & VertexStyleStorage;

export type EdgeTypeConfig = {
/**
Expand All @@ -89,7 +89,7 @@ export type EdgeTypeConfig = {
* Total number of edges of this type
*/
total?: number;
} & EdgePreferencesStorageModel;
} & EdgeStyleStorage;

export type PrefixTypeConfig = {
prefix: RdfPrefix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
} from "@/core/entities";
import { RESERVED_TYPES_PROPERTY } from "@/utils";
import {
createRandomEdgePreferencesStorageModel,
createRandomEdgeStyleStorage,
createRandomEdgeTypeConfig,
createRandomRawConfiguration,
createRandomSchema,
createRandomVertexPreferencesStorageModel,
createRandomVertexStyleStorage,
createRandomVertexTypeConfig,
} from "@/utils/testing";

Expand All @@ -23,11 +23,8 @@ import type {
RawConfiguration,
VertexTypeConfig,
} from "../ConfigurationProvider";
import type { EdgeStyleStorage, VertexStyleStorage } from "./graphStyles";
import type { SchemaStorageModel } from "./schema";
import type {
EdgePreferencesStorageModel,
VertexPreferencesStorageModel,
} from "./userPreferences";

import {
activeConfigSelector,
Expand All @@ -42,14 +39,14 @@ import {
} from "./configuration";

function toVertexStyles(
styles: VertexPreferencesStorageModel[] = [],
): Map<VertexType, VertexPreferencesStorageModel> {
styles: VertexStyleStorage[] = [],
): Map<VertexType, VertexStyleStorage> {
return new Map(styles.map(style => [style.type, style]));
}

function toEdgeStyles(
styles: EdgePreferencesStorageModel[] = [],
): Map<EdgeType, EdgePreferencesStorageModel> {
styles: EdgeStyleStorage[] = [],
): Map<EdgeType, EdgeStyleStorage> {
return new Map(styles.map(style => [style.type, style]));
}

Expand Down Expand Up @@ -143,13 +140,13 @@ describe("mergedConfiguration", () => {
const schema = createRandomSchema();
const vertexStyles = toVertexStyles(
schema.vertices.map(v => ({
...createRandomVertexPreferencesStorageModel(),
...createRandomVertexStyleStorage(),
type: v.type,
})),
);
const edgeStyles = toEdgeStyles(
schema.edges.map(v => ({
...createRandomEdgePreferencesStorageModel(),
...createRandomEdgeStyleStorage(),
type: v.type,
})),
);
Expand Down
Loading
Loading