phase 3: runes mode for all

This commit is contained in:
Smit 2025-06-26 12:24:03 +05:30
parent c07f4b32af
commit 10d6b574c3
20 changed files with 243 additions and 181 deletions

View file

@ -5,7 +5,7 @@
import Editor from "@graphite/components/Editor.svelte";
let editor: GraphiteEditor | undefined = undefined;
let editor: GraphiteEditor | undefined = $state(undefined);
onMount(async () => {
await initWasm();

View file

@ -19,8 +19,13 @@
import MainWindow from "@graphite/components/window/MainWindow.svelte";
// Graphite WASM editor
export let editor: Editor;
interface Props {
// Graphite WASM editor
editor: Editor;
}
let { editor }: Props = $props();
setContext("editor", editor);
// State provider systems

View file

@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
// Should be equal to the width and height of the zoom preview canvas in the CSS
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
// Should be equal to the width and height of the `.pixel-outline` div in the CSS, and should be evenly divisible into the number above
@ -8,24 +8,32 @@
</script>
<script lang="ts">
import { onMount } from "svelte";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
const temporaryCanvas = document.createElement("canvas");
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
let zoomPreviewCanvas: HTMLCanvasElement | undefined;
let zoomPreviewCanvas: HTMLCanvasElement | undefined = $state();
export let imageData: ImageData | undefined = undefined;
export let colorChoice: string;
export let primaryColor: string;
export let secondaryColor: string;
export let x: number;
export let y: number;
interface Props {
imageData?: ImageData | undefined;
colorChoice: string;
primaryColor: string;
secondaryColor: string;
x: number;
y: number;
}
let {
imageData = undefined,
colorChoice,
primaryColor,
secondaryColor,
x,
y
}: Props = $props();
$: displayImageDataPreview(imageData);
function displayImageDataPreview(imageData: ImageData | undefined) {
if (!zoomPreviewCanvas) return;
@ -43,7 +51,7 @@
context.drawImage(temporaryCanvas, 0, 0);
}
onMount(() => {
$effect(() => {
displayImageDataPreview(imageData);
});
</script>
@ -56,8 +64,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

@ -1,5 +1,8 @@
<script lang="ts">
import { createEventDispatcher, getContext, onMount } from "svelte";
import { createBubbler, stopPropagation, passive } from 'svelte/legacy';
const bubble = createBubbler();
import { getContext, onMount } from "svelte";
import type { FrontendNodeType } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
@ -8,16 +11,19 @@
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const dispatch = createEventDispatcher<{ selectNodeType: string }>();
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
export let disabled = false;
export let initialSearchTerm = "";
interface Props {
disabled?: boolean;
initialSearchTerm?: string;
onselectNodeType?: (selectNodeType: string) => void;
}
let nodeSearchInput: TextInput | undefined = undefined;
let searchTerm = initialSearchTerm;
let { disabled = false, initialSearchTerm = "", onselectNodeType }: Props = $props();
let nodeSearchInput: TextInput | undefined = $state(undefined);
let searchTerm = $state(initialSearchTerm);
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
type NodeCategoryDetails = {
nodes: FrontendNodeType[];
@ -107,18 +113,19 @@
onMount(() => {
setTimeout(() => nodeSearchInput?.focus(), 0);
});
let nodeCategories = $derived(buildNodeCategories($nodeGraph.nodeTypes, searchTerm));
</script>
<div class="node-catalog">
<TextInput placeholder="Search Nodes..." bind:value={searchTerm} bind:this={nodeSearchInput} />
<div class="list-results" on:wheel|passive|stopPropagation>
<div class="list-results" use:passive={['wheel', () => stopPropagation(bubble('wheel'))]}>
{#each nodeCategories as nodeCategory}
<details open={nodeCategory[1].open}>
<summary>
<TextLabel>{nodeCategory[0]}</TextLabel>
</summary>
{#each nodeCategory[1].nodes as nodeType}
<TextButton {disabled} label={nodeType.name} tooltip={$nodeGraph.nodeDescriptions.get(nodeType.name)} action={() => dispatch("selectNodeType", nodeType.name)} />
<TextButton {disabled} label={nodeType.name} tooltip={$nodeGraph.nodeDescriptions.get(nodeType.name)} action={() => onselectNodeType?.(nodeType.name)} />
{/each}
</details>
{:else}

View file

@ -29,70 +29,70 @@
import ScrollbarInput from "@graphite/components/widgets/inputs/ScrollbarInput.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
let rulerHorizontal: RulerInput | undefined;
let rulerVertical: RulerInput | undefined;
let viewport: HTMLDivElement | undefined;
let rulerHorizontal: RulerInput | undefined = $state();
let rulerVertical: RulerInput | undefined = $state();
let viewport: HTMLDivElement | undefined = $state();
const editor = getContext<Editor>("editor");
const document = getContext<DocumentState>("document");
// Interactive text editing
let textInput: undefined | HTMLDivElement = undefined;
let showTextInput: boolean;
let textInputMatrix: number[];
let textInput: undefined | HTMLDivElement = $state(undefined);
let showTextInput: boolean = $state(false);
let textInputMatrix: number[] = $state([]);
// Scrollbars
let scrollbarPos: XY = { x: 0.5, y: 0.5 };
let scrollbarSize: XY = { x: 0.5, y: 0.5 };
let scrollbarPos: XY = $state({ x: 0.5, y: 0.5 });
let scrollbarSize: XY = $state({ x: 0.5, y: 0.5 });
let scrollbarMultiplier: XY = { x: 0, y: 0 };
// Rulers
let rulerOrigin: XY = { x: 0, y: 0 };
let rulerSpacing = 100;
let rulerInterval = 100;
let rulersVisible = true;
let rulerOrigin: XY = $state({ x: 0, y: 0 });
let rulerSpacing = $state(100);
let rulerInterval = $state(100);
let rulersVisible = $state(true);
// Rendered SVG viewport data
let artworkSvg = "";
let artworkSvg = $state("");
// Rasterized SVG viewport data, or none if it's not up-to-date
let rasterizedCanvas: HTMLCanvasElement | undefined = undefined;
let rasterizedContext: CanvasRenderingContext2D | undefined = undefined;
// Cursor icon to display while hovering over the canvas
let canvasCursor = "default";
let canvasCursor = $state("default");
// Cursor position for cursor floating menus like the Eyedropper tool zoom
let cursorLeft = 0;
let cursorTop = 0;
let cursorEyedropper = false;
let cursorEyedropperPreviewImageData: ImageData | undefined = undefined;
let cursorEyedropperPreviewColorChoice = "";
let cursorEyedropperPreviewColorPrimary = "";
let cursorEyedropperPreviewColorSecondary = "";
let cursorLeft = $state(0);
let cursorTop = $state(0);
let cursorEyedropper = $state(false);
let cursorEyedropperPreviewImageData: ImageData | undefined = $state(undefined);
let cursorEyedropperPreviewColorChoice = $state("");
let cursorEyedropperPreviewColorPrimary = $state("");
let cursorEyedropperPreviewColorSecondary = $state("");
// Canvas dimensions
let canvasSvgWidth: number | undefined = undefined;
let canvasSvgHeight: number | undefined = undefined;
let canvasSvgWidth: number | undefined = $state(undefined);
let canvasSvgHeight: number | undefined = $state(undefined);
let devicePixelRatio: number | undefined;
let devicePixelRatio: number | undefined = $state();
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
$: canvasWidthRoundedToEven = canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth);
$: canvasHeightRoundedToEven = canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight);
let canvasWidthRoundedToEven = $derived(canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth));
let canvasHeightRoundedToEven = $derived(canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight));
// Used to set the canvas element size on the page.
// The value above in pixels, or if undefined, we fall back to 100% as a non-pixel-perfect backup that's hopefully short-lived
$: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%";
$: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%";
let canvasWidthCSS = $derived(canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%");
let canvasHeightCSS = $derived(canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%");
$: canvasWidthScaled = canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio);
$: canvasHeightScaled = canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio);
let canvasWidthScaled = $derived(canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio));
let canvasHeightScaled = $derived(canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio));
// Used to set the canvas rendering dimensions.
$: canvasWidthScaledRoundedToEven = canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled);
$: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled);
let canvasWidthScaledRoundedToEven = $derived(canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled));
let canvasHeightScaledRoundedToEven = $derived(canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled));
$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
let toolShelfTotalToolsAndSeparators = $derived(((layoutGroup) => {
if (!isWidgetSpanRow(layoutGroup)) return undefined;
let totalSeparators = 0;
@ -124,7 +124,7 @@
totalToolRowsFor2Columns,
totalToolRowsFor3Columns,
};
})($document.toolShelfLayout.layout[0]);
})($document.toolShelfLayout.layout[0]));
function dropFile(e: DragEvent) {
const { dataTransfer } = e;
@ -523,7 +523,7 @@
</svg>
<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})" onscroll={preventTextEditingScroll} />
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" onscroll={preventTextEditingScroll}></div>
{/if}
</div>
<canvas

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { handlers } from 'svelte/legacy';
import { getContext, onMount, onDestroy, tick } from "svelte";
import type { Editor } from "@graphite/editor";
@ -43,26 +45,26 @@
const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
let list: LayoutCol | undefined;
let list: LayoutCol | undefined = $state();
// Layer data
let layerCache = new Map<string, LayerPanelEntry>(); // TODO: replace with BigUint64Array as index
let layers: LayerListingInfo[] = [];
let layers: LayerListingInfo[] = $state([]);
// Interactive dragging
let draggable = true;
let draggingData: undefined | DraggingData = undefined;
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
let dragInPanel = false;
let draggable = $state(true);
let draggingData: undefined | DraggingData = $state(undefined);
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = $state(undefined);
let dragInPanel = $state(false);
// Interactive clipping
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
let layerToClipAltKeyPressed = false;
let layerToClipUponClick: LayerListingInfo | undefined = $state(undefined);
let layerToClipAltKeyPressed = $state(false);
// Layouts
let layersPanelControlBarLeftLayout = defaultWidgetLayout();
let layersPanelControlBarRightLayout = defaultWidgetLayout();
let layersPanelBottomBarLayout = defaultWidgetLayout();
let layersPanelControlBarLeftLayout = $state(defaultWidgetLayout());
let layersPanelControlBarRightLayout = $state(defaultWidgetLayout());
let layersPanelBottomBarLayout = $state(defaultWidgetLayout());
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
@ -530,7 +532,7 @@
title={listing.entry.expanded
? "Collapse (Click) / Collapse All (Alt Click)"
: `Expand (Click) / Expand All (Alt Click)${listing.entry.ancestorOfSelected ? "\n(A selected layer is contained within)" : ""}`}
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
onclick={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0"
></button>
{:else}
@ -554,10 +556,12 @@
value={listing.entry.alias}
placeholder={listing.entry.name}
disabled={!listing.editingName}
on:blur={() => onEditLayerNameDeselect(listing)}
on:keydown={(e) => e.key === "Escape" && onEditLayerNameDeselect(listing)}
on:keydown={(e) => e.key === "Enter" && onEditLayerNameChange(listing, e)}
on:change={(e) => onEditLayerNameChange(listing, e)}
onblur={() => onEditLayerNameDeselect(listing)}
onkeydown={(e) => {
e.key === "Escape" && onEditLayerNameDeselect(listing);
e.key === "Enter" && onEditLayerNameChange(listing, e);
}}
onchange={(e) => onEditLayerNameChange(listing, e)}
/>
</LayoutRow>
{#if !listing.entry.unlocked || !listing.entry.parentsUnlocked}
@ -584,7 +588,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

@ -9,7 +9,7 @@
const editor = getContext<Editor>("editor");
let propertiesSectionsLayout = defaultWidgetLayout();
let propertiesSectionsLayout = $state(defaultWidgetLayout());
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {

View file

@ -26,21 +26,16 @@
const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
let graph: HTMLDivElement | undefined;
let graph: HTMLDivElement | undefined = $state();
// Key value is node id + input/output index
// Imports/Export are stored at a key value of 0
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
$: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
let inputElement = $state<HTMLInputElement>();
let hoveringImportIndex = $state<number>();
let hoveringExportIndex = $state<number>()
let inputElement: HTMLInputElement;
let hoveringImportIndex: number | undefined = undefined;
let hoveringExportIndex: number | undefined = undefined;
let editingNameImportIndex: number | undefined = undefined;
let editingNameExportIndex: number | undefined = undefined;
let editingNameText = "";
let editingNameImportIndex = $state<number>();
let editingNameExportIndex = $state<number>();
let editingNameText = $state<string>("");
function exportsToEdgeTextInputWidth() {
let exportTextDivs = document.querySelectorAll(`[data-export-text-edge]`);
@ -230,6 +225,11 @@
}
return result;
}
// Key value is node id + input/output index
// Imports/Export are stored at a key value of 0
let gridSpacing = $derived(calculateGridSpacing($nodeGraph.transform.scale));
let dotRadius = $derived(1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2);
</script>
<div
@ -252,9 +252,9 @@
}}
>
{#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
<NodeCatalog on:selectNodeType={(e) => createNode(e.detail)} />
<NodeCatalog onselectNodeType={(e) => createNode(e)} />
{:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData}
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} onselectNodeType={(e) => createNode(e)} />
{:else}
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
<LayoutRow class="toggle-layer-or-node">
@ -356,8 +356,8 @@
<div
class="edit-import-export import"
on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerleave={() => (hoveringImportIndex = undefined)}
onpointerenter={() => (hoveringImportIndex = index)}
onpointerleave={() => (hoveringImportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
@ -368,11 +368,11 @@
style:width={importsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingImportName}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
onblur={setEditingImportName}
onkeydown={(e) => e.key === "Enter" && setEditingImportName(e)}
/>
{:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p>
<p class="import-text" ondblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p>
{/if}
{#if hoveringImportIndex === index || editingNameImportIndex === index}
<IconButton
@ -394,7 +394,7 @@
x: Number($nodeGraph.imports[0].position.x),
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} />
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24}></div>
{/if}
{#if $nodeGraph.addImport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}>
@ -428,8 +428,8 @@
</svg>
<div
class="edit-import-export export"
on:pointerenter={() => (hoveringExportIndex = index)}
on:pointerleave={() => (hoveringExportIndex = undefined)}
onpointerenter={() => (hoveringExportIndex = index)}
onpointerleave={() => (hoveringExportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
@ -452,11 +452,11 @@
style:width={exportsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingExportName}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
onblur={setEditingExportName}
onkeydown={(e) => e.key === "Enter" && setEditingExportName(e)}
/>
{:else}
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p>
<p class="export-text" ondblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p>
{/if}
</div>
{/each}
@ -465,7 +465,7 @@
x: Number($nodeGraph.exports[0].position.x),
y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24} />
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24}></div>
{/if}
{#if $nodeGraph.addExport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>

View file

@ -6,10 +6,14 @@
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
import WidgetTable from "@graphite/components/widgets/WidgetTable.svelte";
export let layout: WidgetLayout;
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
interface Props {
layout: WidgetLayout;
class?: string;
classes?: Record<string, boolean>;
}
let { layout, class: className = "", classes = {} }: Props = $props();
</script>
{#each layout.layout as layoutGroup}

View file

@ -1,4 +1,7 @@
<script lang="ts">
import WidgetSection from './WidgetSection.svelte';
import { stopPropagation } from 'svelte/legacy';
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
@ -9,23 +12,33 @@
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
export let widgetData: WidgetSectionFromJsMessages;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let layoutTarget: any; // TODO: Give type
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
interface Props {
widgetData: WidgetSectionFromJsMessages;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layoutTarget: any; // TODO: Give type
class?: string;
classes?: Record<string, boolean>;
}
let expanded = true;
let {
widgetData,
layoutTarget,
class: className = "",
classes = {}
}: Props = $props();
let expanded = $state(true);
const editor = getContext<Editor>("editor");
</script>
<!-- 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" />
<button class="header" class:expanded onclick={stopPropagation(() => (expanded = !expanded))} tabindex="0">
<div class="expand-arrow"></div>
<TextLabel tooltip={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
<IconButton
icon={widgetData.pinned ? "PinActive" : "PinInactive"}
@ -67,7 +80,7 @@
{:else if isWidgetSpanColumn(layoutGroup)}
<TextLabel styles={{ color: "#d6536e" }}>Error: The WidgetSpan used here should be a row not a column</TextLabel>
{:else if isWidgetSection(layoutGroup)}
<svelte:self widgetData={layoutGroup} {layoutTarget} />
<WidgetSection widgetData={layoutGroup} {layoutTarget} />
{:else}
<TextLabel styles={{ color: "#d6536e" }}>Error: The widget that belongs here has an invalid layout group type</TextLabel>
{/if}

View file

@ -30,21 +30,21 @@
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
interface Props {
widgetData: WidgetSpanRow | WidgetSpanColumn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layoutTarget: any;
class?: string;
classes?: Record<string, boolean>;
}
export let widgetData: WidgetSpanRow | WidgetSpanColumn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let layoutTarget: any;
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
$: extraClasses = Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" ");
$: direction = watchDirection(widgetData);
$: widgets = watchWidgets(widgetData);
let {
widgetData,
layoutTarget,
class: className = "",
classes = {}
}: Props = $props();
function watchDirection(widgetData: WidgetSpanRow | WidgetSpanColumn): "row" | "column" | undefined {
if (isWidgetSpanRow(widgetData)) return "row";
@ -77,6 +77,11 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
}
let extraClasses = $derived(Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "));
let direction = $derived(watchDirection(widgetData));
let widgets = $derived(watchWidgets(widgetData));
</script>
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
@ -130,7 +135,7 @@
{/if}
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
{#if nodeCatalog}
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(index, e.detail)} />
<NodeCatalog {...exclude(nodeCatalog)} onselectNodeType={(e) => widgetValueCommitAndUpdate(index, e)} />
{/if}
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
{#if numberInput}

View file

@ -3,21 +3,28 @@
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
export let widgetData: WidgetTableFromJsMessages;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let layoutTarget: any; // TODO: Give this a real type
interface Props {
widgetData: WidgetTableFromJsMessages;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layoutTarget: any; // TODO: Give this a real type
}
let { widgetData, layoutTarget }: Props = $props();
</script>
<table>
{#each widgetData.tableWidgets as row}
<tr>
{#each row as cell}
<td>
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} />
</td>
{/each}
</tr>
{/each}
<tbody>
{#each widgetData.tableWidgets as row}
<tr>
{#each row as cell}
<td>
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} />
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<style lang="scss">

View file

@ -11,7 +11,7 @@
const editor = getContext<Editor>("editor");
let hintData: HintData = [];
let hintData: HintData = $state([]);
function inputKeysForPlatform(hint: HintInfo): LayoutKeysGroup[] {
if (platformIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;

View file

@ -13,8 +13,12 @@
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte";
export let platform: Graphite.Platform;
export let maximized: boolean;
interface Props {
platform: Graphite.Platform;
maximized: boolean;
}
let { platform, maximized }: Props = $props();
const editor = getContext<Editor>("editor");
const portfolio = getContext<PortfolioState>("portfolio");
@ -29,11 +33,11 @@
[ACCEL_KEY, "Shift", "KeyT"],
];
let entries: MenuListEntry[] = [];
let entries: MenuListEntry[] = $state([]);
$: docIndex = $portfolio.activeDocumentIndex;
$: displayName = $portfolio.documents[docIndex]?.displayName || "";
$: windowTitle = `${displayName}${displayName && " - "}Graphite`;
let docIndex = $derived($portfolio.activeDocumentIndex);
let displayName = $derived($portfolio.documents[docIndex]?.displayName || "");
let windowTitle = $derived(`${displayName}${displayName && " - "}Graphite`);
onMount(() => {
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);

View file

@ -1,13 +1,17 @@
<script lang="ts">
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
export let maximized = false;
interface Props {
maximized?: boolean;
}
let { maximized = false }: Props = $props();
</script>
<LayoutRow class="window-buttons-mac">
<div class="close" title="Close" />
<div class="minimize" title={maximized ? "Minimize" : "Maximize"} />
<div class="zoom" title="Zoom" />
<div class="close" title="Close"></div>
<div class="minimize" title={maximized ? "Minimize" : "Maximize"}></div>
<div class="zoom" title="Zoom"></div>
</LayoutRow>
<style lang="scss" global>

View file

@ -9,7 +9,7 @@
const fullscreen = getContext<FullscreenState>("fullscreen");
$: requestFullscreenHotkeys = fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked;
let requestFullscreenHotkeys = $derived(fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked);
async function handleClick() {
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();

View file

@ -2,7 +2,11 @@
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
export let maximized = false;
interface Props {
maximized?: boolean;
}
let { maximized = false }: Props = $props();
</script>
<LayoutRow class="window-button windows minimize" tooltip="Minimize">

View file

@ -2,7 +2,11 @@
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
export let text: string;
interface Props {
text: string;
}
let { text }: Props = $props();
</script>
<LayoutRow class="window-title">

View file

@ -22,21 +22,12 @@
/* └─ */ layers: 55,
};
let panelSizes = PANEL_SIZES;
let documentPanel: Panel | undefined;
let panelSizes = $state(PANEL_SIZES);
let documentPanel: Panel | undefined = $state();
let gutterResizeRestore: [number, number] | undefined = undefined;
let pointerCaptureId: number | undefined = undefined;
$: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
$: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => {
const name = doc.displayName;
if (!editor.handle.inDevelopmentMode()) return { name };
const tooltip = `Document ID: ${doc.id}`;
return { name, tooltip };
});
const editor = getContext<Editor>("editor");
const portfolio = getContext<PortfolioState>("portfolio");
@ -130,6 +121,17 @@
addListeners();
}
$effect(() => {
documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
});
let documentTabLabels = $derived($portfolio.documents.map((doc: FrontendDocumentDetails) => {
const name = doc.displayName;
if (!editor.handle.inDevelopmentMode()) return { name };
const tooltip = `Document ID: ${doc.id}`;
return { name, tooltip };
}));
</script>
<LayoutRow class="workspace" data-workspace>

View file

@ -34,16 +34,7 @@ const ALLOWED_LICENSES = [
"NCSA",
];
const runesGlobs = [
"**/components/layout/*.svelte",
"**/components/widgets/labels/*.svelte",
"**/components/widgets/buttons/*.svelte",
"**/components/widgets/inputs/*.svelte",
"**/components/floating-menus/MenuList.svelte",
"**/components/floating-menus/ColorPicker.svelte",
"**/components/floating-menus/Dialog.svelte",
"**/components/window/workspace/Panel.svelte",
];
const runesGlobs = ["**/*.svelte"];
function forceRunes(filePath: string): boolean {
const relativePath = filePath.slice(filePath.indexOf("src"));