Bump frontend dependencies to Svelte 5 (#3648)

* Add more recommended VS Code default configs

* Upgrade frontend dependencies including Svelte 5

* Fix derived_references_self runtime error

* Fix lint warnings
This commit is contained in:
Keavon Chambers 2026-01-17 01:50:14 -08:00 committed by Timon Schelling
parent 3b55064f44
commit 915a344a05
24 changed files with 971 additions and 792 deletions

View File

@ -48,7 +48,7 @@ let
npmDeps = pkgs.fetchNpmDeps {
inherit (info) pname version;
src = "${info.src}/frontend";
hash = "sha256-D8VCNK+Ca3gxO+5wriBn8FszG8/x8n/zM6/MPo9E2j4=";
hash = "sha256-WlwzWGoFi3hjRuM5ucrgavko/gg4iFAwMc6uMLjT/FI=";
};
npmRoot = "frontend";

View File

@ -11,9 +11,10 @@
// Code quality
"wayou.vscode-todo-highlight",
"streetsidesoftware.code-spell-checker",
// Helpful
// Git
"mhutchie.git-graph",
"qezhu.gitlink",
// Helpful
"wmaurer.change-case"
]
}

25
.vscode/settings.json vendored
View File

@ -32,6 +32,7 @@
"editor.formatOnSave": false
},
// Rust Analyzer config
"rust-analyzer.check.command": "clippy",
"rust-analyzer.cargo.allTargets": false,
"rust-analyzer.procMacro.ignored": {
"serde_derive": ["Serialize", "Deserialize"],
@ -47,13 +48,33 @@
"vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_no_noninteractive_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
},
// Git Graph config
"git-graph.repository.fetchAndPrune": true,
"git-graph.repository.showRemoteHeads": false,
"git-graph.repository.commits.fetchAvatars": true,
// VS Code Git config
"git.autofetch": true,
"git.enableStatusBarSync": false,
"git.showActionButton": {
"sync": false
},
// CSpell config
"cSpell.language": "en-US",
"cSpell.logLevel": "Information",
"cSpell.allowCompoundWords": true,
// VS Code config
"html.format.wrapLineLength": 200,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.associations": {
"*.graphite": "json"
}
},
"editor.renderWhitespace": "boundary",
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+",
"evenBetterToml.formatter.alignComments": false
}

View File

@ -32,9 +32,6 @@ export default defineConfig([
"import/resolver": { typescript: true, node: true },
},
languageOptions: {
parserOptions: {
project: "./tsconfig.json",
},
globals: {
...globals.browser,
...globals.node,

View File

@ -32,7 +32,11 @@ if (isInstallNeeded()) {
console.log("Finished installing npm packages.");
} catch (_) {
// eslint-disable-next-line no-console
console.error("Failed to install npm packages. Please delete the `node_modules` folder and run `npm install` from the `/frontend` directory.");
console.error(
"\n\n" +
"------------------------------------------------------------> " +
"Failed to install npm packages. Please delete the `node_modules` folder and run `npm install` from the `/frontend` directory.",
);
process.exit(1);
}
} else {

File diff suppressed because it is too large Load Diff

View File

@ -38,32 +38,32 @@
"source-sans": "github:adobe-fonts/source-sans#2.045R-ro%2F1.095R-it"
},
"devDependencies": {
"@eslint/compat": "^1.3.2",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.34.0",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@types/node": "^24.3.0",
"@eslint/compat": "^2.0.1",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/node": "^25.0.9",
"buffer": "^6.0.3",
"concurrently": "^9.2.1",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-svelte": "^3.11.0",
"globals": "^16.3.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.0.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier": "^3.8.0",
"prettier-plugin-svelte": "^3.4.1",
"process": "^0.11.10",
"rollup-plugin-license": "^3.6.0",
"sass": "^1.91.0",
"svelte": "4.2.20",
"sass": "^1.97.2",
"svelte": "5.46.4",
"svelte-preprocess": "^6.0.3",
"tar": "^7.5.2",
"tar": "^7.5.3",
"ts-node": "^10.9.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.41.0",
"vite": "^5.4.19",
"vite-multiple-assets": "2.2.5"
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0",
"vite": "^7.3.1",
"vite-multiple-assets": "2.2.6"
},
"homepage": "https://graphite.art",
"license": "Apache-2.0",

View File

@ -439,7 +439,7 @@
data-saturation-value-picker
>
{#if !isNone}
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`}></div>
{/if}
{#if alignedAxis}
<div
@ -448,7 +448,7 @@
class:value={alignedAxis === "value"}
style:top={`${(1 - value) * 100}%`}
style:left={`${saturation * 100}%`}
/>
></div>
{/if}
</LayoutCol>
<LayoutCol
@ -459,7 +459,7 @@
data-hue-picker
>
{#if !isNone}
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`}></div>
{/if}
</LayoutCol>
<LayoutCol
@ -470,7 +470,7 @@
data-alpha-picker
>
{#if !isNone}
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`}></div>
{/if}
</LayoutCol>
</LayoutRow>
@ -677,7 +677,7 @@
style:--pure-color-gray={gray}
data-tooltip-label={`Set to ${name}`}
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
/>
></div>
{/each}
</button>
{#if eyedropperSupported()}

View File

@ -56,8 +56,8 @@
>
<div class="ring">
<div class="canvas-container">
<canvas width={ZOOM_WINDOW_DIMENSIONS} height={ZOOM_WINDOW_DIMENSIONS} bind:this={zoomPreviewCanvas} />
<div class="pixel-outline" />
<canvas width={ZOOM_WINDOW_DIMENSIONS} height={ZOOM_WINDOW_DIMENSIONS} bind:this={zoomPreviewCanvas}></canvas>
<div class="pixel-outline"></div>
</div>
</div>
</FloatingMenu>

View File

@ -42,6 +42,7 @@
// Keep the child references outside of the entries array so as to avoid infinite recursion.
let childReferences: MenuList[][] = [];
let openChildValue: string | undefined = undefined;
let search = "";
let reactiveEntries = entries;
let highlighted = activeEntry as MenuListEntry | undefined;
@ -172,7 +173,7 @@
// Close the containing menu
let childReference = getChildReference(menuListEntry);
if (childReference) {
childReference.open = false;
openChildValue = undefined;
reactiveEntries = reactiveEntries;
}
dispatch("open", false);
@ -192,7 +193,7 @@
let childReference = getChildReference(menuListEntry);
if (childReference) {
childReference.open = true;
openChildValue = menuListEntry.value;
reactiveEntries = reactiveEntries;
} else {
dispatch("open", true);
@ -207,19 +208,13 @@
let childReference = getChildReference(menuListEntry);
if (childReference) {
childReference.open = false;
openChildValue = undefined;
reactiveEntries = reactiveEntries;
} else {
dispatch("open", false);
}
}
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
if (!menuListEntry.children?.length) return false;
return getChildReference(menuListEntry)?.open || false;
}
function includeSeparator(entries: MenuListEntry[][], section: MenuListEntry[], sectionIndex: number, search: string): boolean {
const elementsBeforeCurrentSection = entries
.slice(0, sectionIndex)
@ -242,7 +237,7 @@
// No submenu to open
if (!childReference || !highlightedEntry.children?.length) return false;
childReference.open = true;
openChildValue = highlightedEntry.value;
// The reason we bother taking `highlightdEntry` as an argument is because, when this function is called, it can ensure `highlightedEntry` is not undefined.
// But here we still have to set `highlighted` to itself so Svelte knows to reactively update it after we set its `childReference.open` property.
highlighted = highlighted;
@ -262,7 +257,7 @@
const menuOpen = open;
const flatEntries = filteredEntries.flat().filter((entry) => !entry.disabled);
const openChild = flatEntries.findIndex((entry) => (entry.children?.length ?? 0) > 0 && getChildReference(entry)?.open);
const openChild = (openChildValue !== undefined && flatEntries.findIndex((entry) => entry.value === openChildValue)) || -1;
// Allow opening menu with space or enter
if (!menuOpen && (e.key === " " || e.key === "Enter")) {
@ -442,7 +437,7 @@
{#each currentEntries(section, virtualScrollingEntryHeight, virtualScrollingStartIndex, virtualScrollingEndIndex, search) as entry, entryIndex (entryIndex + startIndex)}
<LayoutRow
class="row"
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
classes={{ open: openChildValue === entry.value, active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
styles={{ height: virtualScrollingEntryHeight || "20px" }}
tooltipLabel={entry.tooltipLabel}
tooltipDescription={entry.tooltipDescription}
@ -454,7 +449,7 @@
{#if entry.icon && drawIcon}
<IconLabel icon={entry.icon} iconSizeOverride={16} class="entry-icon" />
{:else if drawIcon}
<div class="no-icon" />
<div class="no-icon"></div>
{/if}
{#if entry.font}
@ -470,7 +465,7 @@
{#if entry.children?.length}
<IconLabel class="submenu-arrow" icon="DropdownArrow" />
{:else}
<div class="no-submenu-arrow" />
<div class="no-submenu-arrow"></div>
{/if}
{#if entry.children}
@ -483,7 +478,7 @@
}}
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
parentsValuePath={[...parentsValuePath, entry.value]}
open={getChildReference(entry)?.open || false}
open={openChildValue === entry.value}
direction="TopRight"
entries={entry.children}
entriesHash={entry.childrenHash || 0n}

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher, getContext, onMount } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import type { FrontendNodeType } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
@ -28,7 +29,7 @@
};
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
const categories = new Map<string, NodeCategoryDetails>();
const categories = new SvelteMap<string, NodeCategoryDetails>();
const isTypeSearch = searchTerm.toLowerCase().startsWith("type:");
let typeSearchTerm = "";
let remainingSearchTerms = [searchTerm.toLowerCase()];

View File

@ -476,7 +476,7 @@
{...$$restProps}
>
{#if displayTail}
<div class="tail" bind:this={tail} />
<div class="tail" bind:this={tail}></div>
{/if}
{#if displayContainer}
<div class="floating-menu-container" bind:this={floatingMenuContainer}>

View File

@ -580,7 +580,7 @@
{/if}
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
{#if showTextInput}
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll}></div>
{/if}
</div>
{#if !$appWindow.viewportHolePunch}

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { getContext, onMount, onDestroy, tick } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import type { Editor } from "@graphite/editor";
import {
@ -54,7 +55,7 @@
let list: LayoutCol | undefined;
// Layer data
let layerCache = new Map<string, LayerPanelEntry>(); // TODO: replace with BigUint64Array as index
let layerCache = new SvelteMap<string, LayerPanelEntry>(); // TODO: replace with BigUint64Array as index
let layers: LayerListingInfo[] = [];
// Interactive dragging
@ -686,7 +687,7 @@
{/each}
</LayoutCol>
{#if draggingData && !draggingData.highlightFolder && dragInPanel}
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`} />
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`}></div>
{/if}
</LayoutRow>
<LayoutRow class="bottom-bar" scrollableX={true}>

View File

@ -349,7 +349,7 @@
}}
/>
{#if index > 0}
<div class="reorder-drag-grip" data-tooltip-description="Reorder this export" />
<div class="reorder-drag-grip" data-tooltip-description="Reorder this export"></div>
{/if}
{/if}
</div>
@ -396,7 +396,7 @@
>
{#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
{#if index > 0}
<div class="reorder-drag-grip" data-tooltip-description="Reorder this export" />
<div class="reorder-drag-grip" data-tooltip-description="Reorder this export"></div>
{/if}
<IconButton
size={16}
@ -457,7 +457,7 @@
x: Number($nodeGraph.updateImportsExports.importPosition.x),
y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24} />
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24}></div>
{/if}
{#if $nodeGraph.reorderExportIndex !== undefined}
@ -465,7 +465,7 @@
x: Number($nodeGraph.updateImportsExports.exportPosition.x),
y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24} />
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24}></div>
{/if}
{/if}
</div>

View File

@ -24,7 +24,7 @@
<!-- TODO: Implement collapsable sections with properties system -->
<LayoutCol class={`widget-section ${className}`.trim()} {classes}>
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
<div class="expand-arrow" />
<div class="expand-arrow"></div>
<TextLabel tooltipLabel={widgetData.name} tooltipDescription={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
<IconButton
icon={widgetData.pinned ? "PinActive" : "PinInactive"}

View File

@ -12,6 +12,7 @@
const dispatch = createEventDispatcher<{ selectedEntryValuePath: string[] }>();
let self: MenuList;
let open = false;
// Note: IconButton should instead be used if only an icon, but no label, is desired.
// However, if multiple TextButton widgets are used in a group with only some having no label, this component is able to accommodate that.
@ -93,9 +94,9 @@
</button>
{#if menuListChildrenExists}
<MenuList
on:open={({ detail }) => self && (self.open = detail)}
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
open={self?.open || false}
on:open={({ detail }) => (open = detail)}
{open}
entries={menuListChildren || []}
entriesHash={menuListChildrenHash || 0n}
direction="Bottom"

View File

@ -114,7 +114,7 @@
on:keydown={(e) => e.key === "Escape" && cancel()}
on:pointerdown
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
/>
></textarea>
{/if}
{#if label}
<label for={`field-input-${id}`} on:pointerdown>{label}</label>

View File

@ -754,9 +754,9 @@
bind:this={inputRangeElement}
/>
{#if rangeSliderClickDragState === "Deciding"}
<div class="fake-slider-thumb" />
<div class="fake-slider-thumb"></div>
{/if}
<div class="slider-progress" />
<div class="slider-progress"></div>
{/if}
{/if}
</FieldInput>

View File

@ -25,15 +25,15 @@
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
>
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("CenterLeft")} class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("Center")} class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("CenterRight")} class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("BottomLeft")} class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("BottomCenter")} class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("BottomRight")} class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("CenterLeft")} class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("Center")} class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("CenterRight")} class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("BottomLeft")} class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("BottomCenter")} class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("BottomRight")} class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div></div></button>
</div>
<style lang="scss" global>

View File

@ -210,7 +210,7 @@
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
<button class="arrow decrease" on:pointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
<div class="scroll-track" on:pointerdown={pressTrack} bind:this={scrollTrack}>
<div class="scroll-thumb" on:pointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
<div class="scroll-thumb" on:pointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight}></div>
</div>
<button class="arrow increase" on:pointerdown={() => pressArrow(1)} tabindex="-1" data-scrollbar-arrow></button>
</div>

View File

@ -8,7 +8,7 @@
<div class={`separator ${direction.toLowerCase()} ${style.toLowerCase()}`}>
{#if style === "Section"}
<div />
<div></div>
{/if}
</div>

View File

@ -4,9 +4,10 @@
// It is needed for class-transformer to work and is imported as a side effect.
// The library replaces the Reflect API on the window to support more features.
import "reflect-metadata";
import { mount } from "svelte";
import App from "@graphite/App.svelte";
document.body.setAttribute("data-app-container", "");
export default new App({ target: document.body });
export default mount(App, { target: document.body });

View File

@ -25,6 +25,9 @@ export default defineConfig(({ mode }) => {
"a11y-no-static-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
"a11y-no-noninteractive-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
"a11y-click-events-have-key-events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
"a11y_consider_explicit_label", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
"a11y_click_events_have_key_events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
"a11y_no_noninteractive_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
];
if (suppressed.includes(warning.code)) return;