Skip to content

Selective style import modal #1970

Description

@kmcginnes

Problem Statement

When users import styles from a file, the current experience is all-or-nothing: a confirmation dialog lists the type names that would be overwritten and offers a single "Load & Replace" button. Users cannot preview what the imported styles look like, cannot choose which types to import, and cannot see what their current styles are before they get replaced. This makes the import action high-risk and low-confidence — users must blindly trust the file.

Additionally, the two-layer styling architecture (shared styles + user customizations) creates confusing terminology ("customizations" vs. "style packs") and a complex danger zone with three different reset buttons. The layering exists to solve a conflict-resolution problem that the new selective import makes unnecessary.

Preferred Solution

Replace the all-or-nothing import dialog with a selective, per-type import modal. The user sees every style in the file as a card with a before→after preview, checks the ones they want, and imports only those. This makes the consequences visible and the action granular.

Collapse the two-layer style architecture (shared + user) into a single user styles layer. Delete the Settings → Styles page and move Save/Load/Reset to the Styles sidebar panel header. Imported styles go directly into user storage — no separate "shared" concept.

User Stories

  1. As a graph explorer user, I want to see a visual preview of each incoming style beside my current style, so that I can understand exactly what will change before I commit to an import.
  2. As a graph explorer user, I want to select which individual type styles to import from a file, so that I can bring in only the styles I want without overwriting ones I've customized.
  3. As a graph explorer user, I want to filter the import list by nodes, edges, or conflicts, so that I can quickly focus on the types I care about in a large file.
  4. As a graph explorer user, I want to search the import list by type name, so that I can find a specific type without scrolling through dozens of cards.
  5. As a graph explorer user, I want identical styles (where the incoming matches my current resolved style) to be automatically excluded from the import list, so that I'm not asked to make decisions about no-ops.
  6. As a graph explorer user, I want a footer summary showing how many styles I've selected, how many are conflicts, and how many were skipped as identical, so that I have full awareness of what the import will do.
  7. As a graph explorer user, I want to trigger the import from the Styles sidebar panel, so that I don't have to navigate to a separate settings page.
  8. As a graph explorer user, I want the import modal to show styles for types not in my current schema, so that I can pre-configure styles before connecting to a new database.
  9. As a graph explorer user, I want a single "Reset to defaults" action in the sidebar, so that I don't have to understand which of three different reset buttons applies to my situation.
  10. As a graph explorer user, I want to save all my styles to a file directly from the sidebar, so that sharing and backup is one click away.
  11. As a graph explorer user, I want to use "Select all" to toggle all currently-filtered types, so that I can bulk-select within a filtered view without affecting types I can't see.
  12. As a graph explorer user, I want the import to show a success toast rather than a confirmation dialog, so that the flow is fast and doesn't require an extra dismissal after I've already made my selections.
  13. As a graph explorer user, I want the card grid to adapt to my window size with responsive columns, so that the modal is usable in both wide browser windows and narrow embedded panels.
  14. As a graph explorer user, I want to see node label settings (display name, description, type override) on each card when present, so that I know the full scope of what's being imported.
  15. As a graph explorer user, I want to dismiss the modal via overlay click, Escape, the close button, or Cancel, so that I can back out easily at any point.

Implementation Decisions

Architecture: single style layer

  • Delete sharedVertexStylesAtom and sharedEdgeStylesAtom from storageAtoms.ts. Remove the corresponding IndexedDB storage keys (shared-vertex-styles, shared-edge-styles).
  • Remove the cascade logic in resolveVertexStyle / resolveEdgeStyle — there is no shared layer to spread. The resolve becomes: {...appDefault, ...user}.
  • No migration is needed. The shared styles feature was never shipped to customers.
  • Delete the Settings → Styles route (routes/Settings/SettingsStyles.tsx) and all its sub-components (LoadStylesButton, ResetAllStylesButton, ResetCustomStylesButton, ResetSharedStylesButton, SaveStylesButton).
  • Remove any "shared styles" references from the Settings sidebar navigation.

Import goes to user storage

  • useApplyStylingImport is rewritten to write selected styles into userVertexStylesAtom / userEdgeStylesAtom instead of the shared atoms.
  • Import is full-type replacement: the entire incoming style entry overwrites whatever existed for that type. No per-field merge.

Conflict detection

  • A "conflict" is defined as: the imported type already has an entry in user storage (userVertexStylesAtom or userEdgeStylesAtom).
  • Types where the incoming resolved style is identical to the existing resolved style (resolved against app defaults, compared as full objects) are excluded from the modal list entirely.
  • The footer shows "N styles already match and were skipped" to communicate why the file's total count differs from what's shown.

Modal structure

  • New module: modules/StyleImport/. Contains the modal, its state machine, and any import-specific components.
  • Uses the existing Dialog component (Radix), not AlertDialog. Overlay-dismissable.
  • DialogContent is widened via className override (not a new variant).
  • Container queries (Tailwind) control responsive column count (3 → 2 → 1).
  • The modal header shows the filename, no count.

Card design

  • Each card shows before→after horizontally: "Current" (or "Default" for new types) on the left, "Incoming" on the right, arrow between.
  • Node preview: shape + icon only — no display label text inside the preview. Monospace type name line below the preview serves as the card's identity.
  • Edge preview: reuse the edge preview component being built in a parallel session. Edge cards span 2 grid columns; node cards span 1.
  • Node label settings (displayName, description, typeOverride) render as a small list below the type name. Lines with unset values are omitted entirely.
  • A checkbox in the card controls selection; the entire card is a click target for its checkbox. Selected cards get a visual ring; deselected cards dim.

Filtering and selection

  • Filter tabs: All / Nodes / Edges / Conflicts — implemented with a new shared ToggleGroup component (Radix/shadcn).
  • Text search: substring match on type name, case-insensitive. Composes with the active filter tab (filter ∩ search).
  • Groups ("Node types · N" / "Edge types · N") always visible; a group disappears when it has zero matches. All filter tabs stay enabled with their counts.
  • "Select all" toggles only the currently-visible (filtered + searched) items. Its checked/indeterminate state reflects visible items only.
  • Selection is the source of truth for import — the import button acts on all selected items regardless of the currently active filter.
  • All types start selected.

Sidebar integration

  • The Styles sidebar panel header gets three buttons: Save, Load, Reset (replacing the gear icon that linked to Settings → Styles).
  • Save: immediately exports all user styles to a file. No picker modal (future follow-up).
  • Load: opens the native file picker → parses → on error shows an AlertDialog (same error states as today: invalid, wrong envelope, empty) → on success opens the selective import modal.
  • Reset: shows a confirmation AlertDialog (existing pattern), then clears both user style atoms to empty maps.

Shared components built for reuse

  • The card, preview, filter, and selection machinery are designed to be mode-agnostic (no import-specific logic baked in), so a future export picker can reuse them.
  • New shared ToggleGroup component from shadcn/Radix.

Flow after import

  • On "Import N selected": write selected styles to user atoms, close the modal, show a success toast.
  • No confirmation dialog after import — the user has already made deliberate selections.

Testing Decisions

What makes a good test here

Tests assert on external behavior: given a parsed file and existing user styles, what ends up in the atoms? Given a certain selection state, what does the import write? Tests should not assert on CSS classes, DOM structure of cards, or internal component state. A purely presentational card with no branching logic needs no test.

Modules to test

  1. The import apply logic (core/styling/useStylingImportExport.ts, rewritten): given a parsed result and a set of selected types, it writes exactly those entries to user atoms, replacing existing ones wholesale. This is the primary behavioral seam — it's a pure function (parsed data + selection → atom writes) exercised through renderHookWithState.

  2. Conflict detection and identical-style exclusion (new logic in core/styling/): given incoming parsed styles and existing user styles, it returns which types are conflicts, which are new, and which are identical (excluded). This is a pure function — unit test directly.

  3. The resolved-style equality check (new utility): given two partial style entries and the app defaults, determine if they resolve to the same full style. Pure function, straightforward unit test.

  4. The export function (useExportStylingFile, simplified): with the shared layer gone, it just dumps user styles. Existing test coverage adapts — remove shared-layer tests, keep the structure.

  5. File parse error handling (parseStylingFile): existing tests cover this; they remain as-is.

Prior art

  • src/core/styling/useStylingImportExport.test.ts — the closest existing test file; tests the hook + atom interaction pattern.
  • src/routes/Settings/LoadStylesButton.test.tsx — integration-level test using userEvent.upload and asserting on dialog content. The new modal's integration test follows a similar shape.
  • src/core/StateProvider/displayVertex.test.ts — canonical hook + DbState pattern for the resolve logic.

Not tested

  • The visual appearance of cards (shape rendering, color, layout).
  • Dark mode styling.
  • Responsive column behavior (container query breakpoints).
  • Animation/transition polish.

Out of Scope

  • Export picker modal: the selective picker for choosing which styles to export is a future follow-up. Save continues to export all styles immediately.
  • Creating/editing individual styles: the Node Style Dialog and Edge Style Dialog are separate features.
  • Server/transport for sharing style files: no cloud sync, auth, or versioning of files.
  • The edge preview component: being built in a parallel session; this feature consumes it.
  • Migration of existing shared styles: the shared styles feature was never shipped; no data migration needed.
  • The ToggleGroup shared component implementation: prerequisite task to be tracked as a sub-issue.

Further Notes

  • The file envelope kind changes from styling-export to styles. No backward compatibility needed — the save/load feature has never shipped to users.
  • The terminology shift: "shared styles" disappears from all code, comments, UI copy, ADRs, and CONTEXT.md. Everything is just "styles." Save/load language replaces apply/import language.
  • Most of the existing shared styles implementation (import hook, conflict detection, parser, tests) moves, renames, and slightly updates rather than being rewritten from scratch. The core logic is sound — it just targets a different atom.

Important

Internal only — this issue is maintained by the core team and is not accepting external contributions.

Metadata

Metadata

Assignees

Labels

internalSignals that the team will work on this issue internally.ready-for-agentfully specified, ready for an AFK agent

Fields

No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions