diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index c71b3ff332..d8ae44eda4 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -19,7 +19,7 @@ module.exports = { parserOptions: { ecmaVersion: "latest", project: "./tsconfig.json", - extraFileExtensions: [".svelte"], + extraFileExtensions: [".svelte", ".svelte.ts"], }, ignorePatterns: [ // Ignore generated directories @@ -33,7 +33,7 @@ module.exports = { ], overrides: [ { - files: ["*.svelte"], + files: ["*.svelte", "*.svelte.ts"], parser: "svelte-eslint-parser", // Parse the ` - + - + {#if !isNone} -
+
{/if} {#if alignedAxis}
+ >
{/if} - + {#if !isNone} -
+
{/if} - + {#if !isNone} -
+
{/if} @@ -447,19 +459,19 @@ { - gradient = gradient; - if (gradient) dispatch("colorOrGradient", gradient); + ongradient={(detail) => { + gradient = detail; + if (gradient) oncolorOrGradient?.(gradient); }} - on:activeMarkerIndexChange={gradientActiveMarkerIndexChange} + onactiveMarkerIndexChange={gradientActiveMarkerIndexChange} activeMarkerIndex={activeIndex} - on:dragging={({ detail }) => (gradientSpectrumDragging = detail)} + bind:drag={gradientSpectrumDragging} bind:this={gradientSpectrumInputWidget} /> {#if gradientSpectrumInputWidget && activeIndex !== undefined} { + onvalue={(detail) => { if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100); }} displayDecimalPlaces={0} @@ -480,7 +492,7 @@ > {#if !newColor.equals(oldColor)}
- + {/if} {#if !newColor.equals(oldColor)} @@ -499,9 +511,9 @@ { - dispatch("startHistoryTransaction"); + value={newColor.toHexOptionalAlpha() ?? "-"} + oncommitText={(detail) => { + onstartHistoryTransaction?.(); setColorCode(detail); }} centered={true} @@ -514,19 +526,17 @@ RGB - {#each rgbChannels as [channel, strength], index} + {#each rgbChannels as [channel, _strength], index} {#if index > 0} {/if} { - strength = detail; + value={rgbChannels[index][1]} + onvalue={(detail) => { + rgbChannels[index][1] = detail; setColorRGB(channel, detail); }} - on:startHistoryTransaction={() => { - dispatch("startHistoryTransaction"); - }} + {onstartHistoryTransaction} min={0} max={255} minWidth={1} @@ -541,19 +551,17 @@ - {#each hsvChannels as [channel, strength], index} + {#each hsvChannels as [channel, _strength], index} {#if index > 0} {/if} { - strength = detail; + value={hsvChannels[index][1]} + onvalue={(detail) => { + hsvChannels[index][1] = detail; setColorHSV(channel, detail); }} - on:startHistoryTransaction={() => { - dispatch("startHistoryTransaction"); - }} + {onstartHistoryTransaction} min={0} max={channel === "h" ? 360 : 100} unit={channel === "h" ? "°" : "%"} @@ -573,13 +581,11 @@ { + onvalue={(detail) => { if (detail !== undefined) alpha = detail / 100; setColorAlphaPercent(detail); }} - on:startHistoryTransaction={() => { - dispatch("startHistoryTransaction"); - }} + {onstartHistoryTransaction} min={0} max={100} rangeMin={0} @@ -593,23 +599,23 @@ {#if allowNone && !gradient} - + {/if} - + - + - - +
diff --git a/frontend/src/components/floating-menus/Dialog.svelte b/frontend/src/components/floating-menus/Dialog.svelte index e9d0ba5fab..9157922271 100644 --- a/frontend/src/components/floating-menus/Dialog.svelte +++ b/frontend/src/components/floating-menus/Dialog.svelte @@ -16,7 +16,7 @@ const dialog = getContext("dialog"); - let self: FloatingMenu | undefined; + let self: FloatingMenu | undefined = $state(); onMount(() => { // Focus the button which is marked as emphasized, or otherwise the first button, in the popup @@ -41,14 +41,14 @@
The editor crashed — sorry about that
Please report this by filing an issue on GitHub:
-
window.open(githubUrl($dialog.panicDetails), "_blank")} />
+
window.open(githubUrl($dialog.panicDetails), "_blank")} />
Reload the editor to continue. If this occurs
immediately on repeated reloads, clear storage:
{ + onclick={async () => { await wipeDocuments(); window.location.reload(); }} @@ -68,8 +68,8 @@ {/if} {#if $dialog.panicDetails} - navigator.clipboard.writeText($dialog.panicDetails)} /> - window.location.reload()} /> + navigator.clipboard.writeText($dialog.panicDetails)} /> + window.location.reload()} /> {/if} diff --git a/frontend/src/components/floating-menus/EyedropperPreview.svelte b/frontend/src/components/floating-menus/EyedropperPreview.svelte index 4ae69182c1..aafffbbde3 100644 --- a/frontend/src/components/floating-menus/EyedropperPreview.svelte +++ b/frontend/src/components/floating-menus/EyedropperPreview.svelte @@ -1,4 +1,4 @@ - @@ -56,8 +56,8 @@ >
- -
+ +
diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index 0695d38305..c0a182d40b 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -1,9 +1,5 @@ - - (open = detail)} - on:naturalWidth + bind:open type="Dropdown" windowEdgeMargin={0} escapeCloses={false} {direction} {minWidth} scrollableY={scrollableY && virtualScrollingEntryHeight === 0} + {onnaturalWidth} bind:this={self} > {#if search.length > 0} - (search = detail)} bind:this={searchTextInput}> + {/if} {#if virtualScrollingEntryHeight} @@ -424,14 +451,14 @@ classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }} styles={{ height: virtualScrollingEntryHeight || "20px" }} {tooltip} - on:click={() => !entry.disabled && onEntryClick(entry)} - on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)} - on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)} + onclick={() => !entry.disabled && onEntryClick(entry)} + onpointerenter={() => !entry.disabled && onEntryPointerEnter(entry)} + onpointerleave={() => !entry.disabled && onEntryPointerLeave(entry)} > {#if entry.icon && drawIcon} {:else if drawIcon} -
+
{/if} {#if entry.font} @@ -447,18 +474,13 @@ {#if entry.children?.length} {:else} -
+
{/if} {#if entry.children} { - // We do a manual dispatch here instead of just `on:naturalWidth` as a workaround for the
- (searchTerm = detail)} bind:this={nodeSearchInput} /> -
+ +
{ + // onwheel events are passive by default + // https://svelte.dev/docs/svelte/v5-migration-guide#Breaking-changes-in-runes-mode-Touch-and-wheel-events-are-passive + event.stopPropagation(); + onwheel?.(event); + }} + > {#each nodeCategories as nodeCategory}
{nodeCategory[0]} {#each nodeCategory[1].nodes as nodeType} - dispatch("selectNodeType", nodeType.name)} /> + onselectNodeType?.(nodeType.name)} /> {/each}
{:else} diff --git a/frontend/src/components/layout/ConditionalWrapper.svelte b/frontend/src/components/layout/ConditionalWrapper.svelte index 46d5e80b04..c6ba7bac02 100644 --- a/frontend/src/components/layout/ConditionalWrapper.svelte +++ b/frontend/src/components/layout/ConditionalWrapper.svelte @@ -1,14 +1,21 @@ {#if condition}
- + {@render children?.()}
{:else} - + {@render children?.()} {/if} diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index e4070cb153..00fe3d147c 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -1,6 +1,4 @@ - -
+
{#if displayTail} -
+
{/if} {#if displayContainer}
- + {@render children?.()}
{/if} diff --git a/frontend/src/components/layout/LayoutCol.svelte b/frontend/src/components/layout/LayoutCol.svelte index b69b6bfabb..27a04063ca 100644 --- a/frontend/src/components/layout/LayoutCol.svelte +++ b/frontend/src/components/layout/LayoutCol.svelte @@ -1,23 +1,35 @@ - (dragInPanel = false)}> + (dragInPanel = false)}> @@ -498,10 +497,10 @@ styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }} data-layer-panel bind:this={list} - on:click={() => deselectAllLayers()} - on:dragover={updateInsertLine} - on:dragend={drop} - on:drop={drop} + onclick={() => deselectAllLayers()} + ondragover={updateInsertLine} + ondragend={drop} + ondrop={drop} > {#each layers as listing, index} {@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected} @@ -519,8 +518,8 @@ data-index={index} tooltip={listing.entry.tooltip} {draggable} - on:dragstart={(e) => draggable && dragStart(e, listing)} - on:click={(e) => selectLayerWithModifiers(e, listing)} + ondragstart={(e) => draggable && dragStart(e, listing)} + onclick={(e) => selectLayerWithModifiers(e, listing)} > {#if listing.entry.childrenAllowed} {:else} @@ -547,24 +546,26 @@ {#if listing.entry.name === "Artboard"} {/if} - onEditLayerName(listing)}> + onEditLayerName(listing)}> 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) => { + if (e.key === "Escape") onEditLayerNameDeselect(listing); + if (e.key === "Enter") onEditLayerNameChange(listing, e); + }} + onchange={(e) => onEditLayerNameChange(listing, e)} /> {#if !listing.entry.unlocked || !listing.entry.parentsUnlocked} (toggleLayerLock(listing.entry.id), e?.stopPropagation())} + onclick={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())} size={24} icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"} hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"} @@ -574,7 +575,7 @@ (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())} + onclick={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())} size={24} icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"} hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"} @@ -584,7 +585,7 @@ {/each} {#if draggingData && !draggingData.highlightFolder && dragInPanel} -
+
{/if} diff --git a/frontend/src/components/panels/Properties.svelte b/frontend/src/components/panels/Properties.svelte index 72607849a5..738b330a57 100644 --- a/frontend/src/components/panels/Properties.svelte +++ b/frontend/src/components/panels/Properties.svelte @@ -2,19 +2,19 @@ import { getContext, onMount } from "svelte"; import type { Editor } from "@graphite/editor"; - import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; + import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages.svelte"; + import type { WidgetLayout as WidgetLayoutState } from "@graphite/messages.svelte"; const editor = getContext("editor"); - let propertiesSectionsLayout = defaultWidgetLayout(); + let propertiesSectionsLayout = $state(defaultWidgetLayout()); onMount(() => { editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => { patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout); - propertiesSectionsLayout = propertiesSectionsLayout; }); }); diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index eaedc3a6df..ff4a15dcb7 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -4,8 +4,7 @@ import { fade } from "svelte/transition"; import type { Editor } from "@graphite/editor"; - import type { Node } from "@graphite/messages"; - import type { FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages"; + import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { IconName } from "@graphite/utility-functions/icons"; @@ -18,6 +17,8 @@ import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import Separator from "@graphite/components/widgets/labels/Separator.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; + import { type FrontendNode, type FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages.svelte"; + import type { Node } from "@graphite/messages.svelte"; const GRID_COLLAPSE_SPACING = 10; const GRID_SIZE = 24; @@ -26,21 +27,16 @@ const editor = getContext("editor"); const nodeGraph = getContext("nodeGraph"); - let graph: HTMLDivElement | undefined; - - // 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 graph: HTMLDivElement | undefined = $state(); - let inputElement: HTMLInputElement; - let hoveringImportIndex: number | undefined = undefined; - let hoveringExportIndex: number | undefined = undefined; + let inputElement = $state(); + let hoveringImportIndex = $state(); + let hoveringExportIndex = $state(); - let editingNameImportIndex: number | undefined = undefined; - let editingNameExportIndex: number | undefined = undefined; - let editingNameText = ""; + let editingNameImportIndex = $state(); + let editingNameExportIndex = $state(); + let editingNameText = $state(""); + let nodeValues: FrontendNode[] = $state([]); function exportsToEdgeTextInputWidth() { let exportTextDivs = document.querySelectorAll(`[data-export-text-edge]`); @@ -204,7 +200,7 @@ } function primaryOutputConnectedToLayer(node: FrontendNode): boolean { - let firstConnectedNode = Array.from($nodeGraph.nodes.values()).find((n) => + let firstConnectedNode = nodeValues.find((n) => node.primaryOutput?.connectedTo.some((connector) => { if ((connector as Node).nodeId === undefined) return false; if (connector.index !== 0n) return false; @@ -215,7 +211,7 @@ } function primaryInputConnectedToLayer(node: FrontendNode): boolean { - const connectedNode = Array.from($nodeGraph.nodes.values()).find((n) => { + const connectedNode = nodeValues.find((n) => { if ((node.primaryInput?.connectedTo as Node) === undefined) return false; return n.id === (node.primaryInput?.connectedTo as Node).nodeId; }); @@ -230,6 +226,16 @@ } return result; } + + $effect.pre(() => { + nodeValues = Array.from($nodeGraph.nodes.values()); + }); + + // 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);
{#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} - createNode(e.detail)} /> + createNode(e)} /> {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} - createNode(e.detail)} /> + createNode(e)} /> {:else} {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} @@ -282,7 +288,7 @@ - editor.handle.mergeSelectedNodes()} /> + editor.handle.mergeSelectedNodes()} /> {/if} @@ -335,29 +341,11 @@
{#each $nodeGraph.imports as { outputMetadata, position }, index} - - {`${dataTypeTooltip(outputMetadata)}\n\n${outputConnectedToText(outputMetadata)}`} - {#if outputMetadata.connectedTo !== undefined} - - {:else} - - {/if} - - + {@render port("port", "output", outputMetadata, position.x / 24, position.y / 24)}
(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 +356,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} -

setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}

+

setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}

{/if} {#if hoveringImportIndex === index || editingNameImportIndex === index} { + onclick={() => { /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} /> @@ -389,47 +377,31 @@ {/if}
{/each} + {#if $nodeGraph.reorderImportIndex !== undefined} {@const position = { x: Number($nodeGraph.imports[0].position.x), y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24, }} -
+
{/if} {#if $nodeGraph.addImport !== undefined}
{ + onclick={() => { /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} />
{/if} {#each $nodeGraph.exports as { inputMetadata, position }, index} - - {`${dataTypeTooltip(inputMetadata)}\n\n${inputConnectedToText(inputMetadata)}`} - {#if inputMetadata.connectedTo !== undefined} - - {:else} - - {/if} - + {@render port("port", "input", inputMetadata, position.x / 24, position.y / 24)}
(hoveringExportIndex = index)} - on:pointerleave={() => (hoveringExportIndex = undefined)} + onpointerenter={() => (hoveringExportIndex = index)} + onpointerleave={() => (hoveringExportIndex = undefined)} style:--offset-left={position.x / 24} style:--offset-top={position.y / 24} > @@ -441,7 +413,7 @@ class="remove-button-export" data-index={index} data-export-text-edge - action={() => { + onclick={() => { /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} /> @@ -452,11 +424,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} -

setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}

+

setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}

{/if}
{/each} @@ -465,14 +437,14 @@ x: Number($nodeGraph.exports[0].position.x), y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24, }} -
+
{/if} {#if $nodeGraph.addExport !== undefined}
{ + onclick={() => { /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} /> @@ -564,22 +536,7 @@ {#if node.exposedInputs.length > 0}
- - {`${dataTypeTooltip(stackDataInput)}\n\n${validTypesText(stackDataInput)}\n\n${inputConnectedToText(stackDataInput)}`} - {#if stackDataInput.connectedTo !== undefined} - - {:else} - - {/if} - + {@render port("port", "input", stackDataInput)}
{/if}
@@ -592,7 +549,7 @@ data-visibility-button size={24} icon={node.visible ? "EyeVisible" : "EyeHidden"} - action={() => { + onclick={() => { /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} tooltip={node.visible ? "Visible" : "Hidden"} @@ -682,81 +639,19 @@
{#if node.primaryInput?.dataType} - - {`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`} - {#if node.primaryInput.connectedTo !== undefined} - - {:else} - - {/if} - + {@render port("port primary-port", "input", node.primaryInput)} {/if} {#each node.exposedInputs as secondary, index} {#if index < node.exposedInputs.length} - - {`${dataTypeTooltip(secondary)}\n\n${validTypesText(secondary)}\n\n${inputConnectedToText(secondary)}`} - {#if secondary.connectedTo !== undefined} - - {:else} - - {/if} - + {@render port("port", "input", secondary)} {/if} {/each}
- {#if node.primaryOutput} - - {`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`} - {#if node.primaryOutput.connectedTo !== undefined} - - {:else} - - {/if} - - {/if} + {@render port("port primary-port", "output", node.primaryOutput)} {#each node.exposedOutputs as secondary} - - {`${dataTypeTooltip(secondary)}\n\n${outputConnectedToText(secondary)}`} - {#if secondary.connectedTo !== undefined} - - {:else} - - {/if} - + {@render port("port", "output", secondary)} {/each}
@@ -786,6 +681,34 @@ >
{/if} +{#snippet port(className: string, dataPort: string, node?: FrontendGraphInput | FrontendGraphOutput, offsetLeft?: number, offsetTop?: number)} + {#if node} + {@const color = node.dataType.toLowerCase()} + + {#if node instanceof FrontendGraphOutput} + {`${dataTypeTooltip(node)}\n\n${outputConnectedToText(node)}`} + {:else} + {`${dataTypeTooltip(node)}\n\n${validTypesText(node)}\n\n${inputConnectedToText(node)}`} + {/if} + {#if node.connectedTo !== undefined} + + {:else} + + {/if} + + {/if} +{/snippet} + diff --git a/frontend/src/components/widgets/inputs/TextInput.svelte b/frontend/src/components/widgets/inputs/TextInput.svelte index b9a794d140..de518bc01c 100644 --- a/frontend/src/components/widgets/inputs/TextInput.svelte +++ b/frontend/src/components/widgets/inputs/TextInput.svelte @@ -1,54 +1,48 @@ diff --git a/frontend/src/components/widgets/labels/Separator.svelte b/frontend/src/components/widgets/labels/Separator.svelte index 0c210b4d1d..58f943ee21 100644 --- a/frontend/src/components/widgets/labels/Separator.svelte +++ b/frontend/src/components/widgets/labels/Separator.svelte @@ -1,13 +1,17 @@
{#if type === "Section"} -
+
{/if}
diff --git a/frontend/src/components/widgets/labels/TextLabel.svelte b/frontend/src/components/widgets/labels/TextLabel.svelte index 540aa16489..6b452cf738 100644 --- a/frontend/src/components/widgets/labels/TextLabel.svelte +++ b/frontend/src/components/widgets/labels/TextLabel.svelte @@ -1,26 +1,53 @@