Replace text-only tooltips with custom richly styled tooltips (#3436)

* Replace the title attribute with custom FloatingMenu tooltips

* Separate tooltip labels and descriptions into two styled blocks

* Move keyboard shortcut tooltips to a separate section at the bottom

* Update shortcut key styling in tooltips and hints bar

* Fix .to_string()
This commit is contained in:
Keavon Chambers
2025-11-30 13:32:58 -08:00
committed by GitHub
parent 94e5c8fc05
commit e8ebcc2c21
94 changed files with 1323 additions and 580 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ During development when HMR (hot-module replacement) occurs, these are also unmo
TypeScript files which provide reactive state and importable functions to Svelte components. Each module defines a Svelte writable store `const { subscribe, update } = writable({ .. });` and exports the `subscribe` method from the module in the returned object. Other functions may also be defined in the module and exported after `subscribe`, which provide a way for Svelte components to call functions to manipulate the state.
In `Editor.svelte`, an instance of each of these are given to Svelte's [`setContext()`](https://svelte.dev/docs#run-time-svelte-setcontext) function. This allows any component to access the state provider instance using `const exampleStateProvider = getContext<ExampleStateProvider>("exampleStateProvider");`.
In `Editor.svelte`, an instance of each of these are given to Svelte's `setContext()` function. This allows any component to access the state provider instance using `const exampleStateProvider = getContext<ExampleStateProvider>("exampleStateProvider");`.
## _I/O managers vs. state providers_
+4 -1
View File
@@ -3,7 +3,7 @@
import { type Editor } from "@graphite/editor";
import { createClipboardManager } from "@graphite/io-managers/clipboard";
import { createHyperlinkManager } from "@graphite/io-managers/hyperlinks";
import { createHyperlinkManager } from "@graphite/io-managers/hyperlink";
import { createInputManager } from "@graphite/io-managers/input";
import { createLocalizationManager } from "@graphite/io-managers/localization";
import { createPanicManager } from "@graphite/io-managers/panic";
@@ -15,6 +15,7 @@
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
import { createPortfolioState } from "@graphite/state-providers/portfolio";
import { createTooltipState } from "@graphite/state-providers/tooltip";
import { operatingSystem } from "@graphite/utility-functions/platform";
import MainWindow from "@graphite/components/window/MainWindow.svelte";
@@ -26,6 +27,8 @@
// State provider systems
let dialog = createDialogState(editor);
setContext("dialog", dialog);
let tooltip = createTooltipState();
setContext("tooltip", tooltip);
let document = createDocumentState(editor);
setContext("document", document);
let fonts = createFontsState(editor);
@@ -31,6 +31,14 @@
blue: [0, 0, 1],
magenta: [1, 0, 1],
};
const PURE_COLORS_GRAYABLE = [
["Red", "#ff0000", "#4c4c4c"],
["Yellow", "#ffff00", "#e3e3e3"],
["Green", "#00ff00", "#969696"],
["Cyan", "#00ffff", "#b2b2b2"],
["Blue", "#0000ff", "#1c1c1c"],
["Magenta", "#ff00ff", "#696969"],
];
const editor = getContext<Editor>("editor");
@@ -420,7 +428,13 @@
>
<LayoutCol class="pickers-and-gradient">
<LayoutRow class="pickers">
<LayoutCol class="saturation-value-picker" title={disabled ? "Saturation and value (disabled)" : "Saturation and value"} on:pointerdown={onPointerDown} data-saturation-value-picker>
<LayoutCol
class="saturation-value-picker"
data-tooltip-label="Saturation and Value"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
on:pointerdown={onPointerDown}
data-saturation-value-picker
>
{#if !isNone}
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
{/if}
@@ -434,12 +448,24 @@
/>
{/if}
</LayoutCol>
<LayoutCol class="hue-picker" title={disabled ? "Hue (disabled)" : "Hue"} on:pointerdown={onPointerDown} data-hue-picker>
<LayoutCol
class="hue-picker"
data-tooltip-label="Hue"
data-tooltip-description={"The shade along the spectrum of the rainbow." + (disabled ? "\n\nDisabled (read-only)." : "")}
on:pointerdown={onPointerDown}
data-hue-picker
>
{#if !isNone}
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
{/if}
</LayoutCol>
<LayoutCol class="alpha-picker" title={disabled ? "Alpha (disabled)" : "Alpha"} on:pointerdown={onPointerDown} data-alpha-picker>
<LayoutCol
class="alpha-picker"
data-tooltip-label="Alpha"
data-tooltip-description={"The level of translucency." + (disabled ? "\n\nDisabled (read-only)." : "")}
on:pointerdown={onPointerDown}
data-alpha-picker
>
{#if !isNone}
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
{/if}
@@ -479,11 +505,11 @@
class="choice-preview"
classes={{ outlined, transparency }}
styles={{ "--outline-amount": outlineFactor }}
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
tooltipDescription={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before it was changed (right)." : "The present color choice."}
>
{#if !newColor.equals(oldColor) && !disabled}
<div class="swap-button-background"></div>
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltipLabel="Swap" />
{/if}
<LayoutCol class="new-color" classes={{ none: isNone }}>
{#if !newColor.equals(oldColor)}
@@ -496,9 +522,12 @@
</LayoutCol>
{/if}
</LayoutRow>
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltip="Color model, color space, and HDR (coming soon)" /> -->
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltipDescription="Color model, color space, and HDR (coming soon)." /> -->
<LayoutRow>
<TextLabel tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}>Hex</TextLabel>
<TextLabel
tooltipLabel="Hex Color Code"
tooltipDescription={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}>Hex</TextLabel
>
<Separator type="Related" />
<LayoutRow>
<TextInput
@@ -509,13 +538,14 @@
setColorCode(detail);
}}
centered={true}
tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}
tooltipLabel="Hex Color Code"
tooltipDescription={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}
bind:this={hexCodeInputWidget}
/>
</LayoutRow>
</LayoutRow>
<LayoutRow>
<TextLabel tooltip="Red/Green/Blue channels of the color, integers 0255">RGB</TextLabel>
<TextLabel tooltipLabel="Red/Green/Blue" tooltipDescription="Integers 0255.">RGB</TextLabel>
<Separator type="Related" />
<LayoutRow>
{#each rgbChannels as [channel, strength], index}
@@ -535,15 +565,17 @@
min={0}
max={255}
minWidth={1}
tooltip={`${{ r: "Red", g: "Green", b: "Blue" }[channel]} channel, integers 0255`}
tooltipLabel={{ r: "Red Channel", g: "Green Channel", b: "Blue Channel" }[channel]}
tooltipDescription="Integers 0255."
/>
{/each}
</LayoutRow>
</LayoutRow>
<LayoutRow>
<TextLabel tooltip={"Hue/Saturation/Value, also known as Hue/Saturation/Brightness (HSB).\nNot to be confused with Hue/Saturation/Lightness (HSL), a different color model."}>
HSV
</TextLabel>
<TextLabel
tooltipLabel="Hue/Saturation/Value"
tooltipDescription="Also known as Hue/Saturation/Brightness (HSB). Not to be confused with Hue/Saturation/Lightness (HSL), a different color model.">HSV</TextLabel
>
<Separator type="Related" />
<LayoutRow>
{#each hsvChannels as [channel, strength], index}
@@ -565,17 +597,22 @@
unit={channel === "h" ? "°" : "%"}
minWidth={1}
displayDecimalPlaces={1}
tooltip={{
h: `Hue component, the shade along the spectrum of the rainbow`,
s: `Saturation component, the vividness from grayscale to full color`,
v: "Value component, the brightness from black to full color",
tooltipLabel={{
h: "Hue Component",
s: "Saturation Component",
v: "Value Component",
}[channel]}
tooltipDescription={{
h: "The shade along the spectrum of the rainbow.",
s: "The vividness from grayscale to full color.",
v: "The brightness from black to full color.",
}[channel]}
/>
{/each}
</LayoutRow>
</LayoutRow>
<LayoutRow>
<TextLabel tooltip="Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel">Alpha</TextLabel>
<TextLabel tooltipLabel="Alpha" tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%).">Alpha</TextLabel>
<Separator type="Related" />
<NumberInput
value={!isNone ? alpha * 100 : undefined}
@@ -594,29 +631,54 @@
unit="%"
mode="Range"
displayDecimalPlaces={1}
tooltip="Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel"
tooltipLabel="Alpha"
tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%)."
/>
</LayoutRow>
<LayoutRow class="leftover-space" />
<LayoutRow>
{#if allowNone && !gradient}
<button class="preset-color none" {disabled} on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
<button
class="preset-color none"
{disabled}
on:click={() => setColorPreset("none")}
data-tooltip-label="Set to No Color"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
tabindex="0"
></button>
<Separator type="Related" />
{/if}
<button class="preset-color black" {disabled} on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
<button
class="preset-color black"
{disabled}
on:click={() => setColorPreset("black")}
data-tooltip-label="Set to Black"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
tabindex="0"
></button>
<Separator type="Related" />
<button class="preset-color white" {disabled} on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
<button
class="preset-color white"
{disabled}
on:click={() => setColorPreset("white")}
data-tooltip-label="Set to White"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
tabindex="0"
></button>
<Separator type="Related" />
<button class="preset-color pure" {disabled} on:click={setColorPresetSubtile} tabindex="-1">
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" />
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" />
<div data-pure-tile="cyan" style="--pure-color: #00ffff; --pure-color-gray: #b2b2b2" title="Set to cyan" />
<div data-pure-tile="blue" style="--pure-color: #0000ff; --pure-color-gray: #1c1c1c" title="Set to blue" />
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
{#each PURE_COLORS_GRAYABLE as [name, color, gray]}
<div
data-pure-tile={name.toLowerCase()}
style:--pure-color={color}
style:--pure-color-gray={gray}
data-tooltip-label="Set to Red"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
/>
{/each}
</button>
<Separator type="Related" />
<IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
<IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltipLabel="Eyedropper" tooltipDescription="Sample a pixel color from the document." />
</LayoutRow>
</LayoutCol>
</LayoutRow>
@@ -30,7 +30,9 @@
export let interactive = false;
export let scrollableY = false;
export let virtualScrollingEntryHeight = 0;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Keep the child references outside of the entries array so as to avoid infinite recursion.
let childReferences: MenuList[][] = [];
@@ -423,7 +425,9 @@
class="row"
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
styles={{ height: virtualScrollingEntryHeight || "20px" }}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
on:click={() => !entry.disabled && onEntryClick(entry)}
on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
@@ -118,7 +118,13 @@
<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}
tooltipLabel={nodeType.name}
tooltipDescription={$nodeGraph.nodeDescriptions.get(nodeType.name)}
action={() => dispatch("selectNodeType", nodeType.name)}
/>
{/each}
</details>
{:else}
@@ -0,0 +1,81 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const tooltip = getContext<TooltipState>("tooltip");
const editor = getContext<Editor>("editor");
let self: FloatingMenu | undefined;
$: label = filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim());
$: description = filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim());
$: shortcut = filterTodo($tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim());
// TODO: Once all TODOs are replaced with real text, remove this function
function filterTodo(text: string | undefined): string | undefined {
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
return text;
}
</script>
<div class="tooltip" style:top={`${$tooltip.position.y}px`} style:left={`${$tooltip.position.x}px`}>
{#if label || description}
<FloatingMenu open={true} type="Tooltip" direction="Bottom" bind:this={self}>
{#if label || shortcut}
<LayoutRow class="tooltip-header">
{#if label}
<TextLabel class="tooltip-label">{label}</TextLabel>
{/if}
{#if shortcut}
<TextLabel class="tooltip-shortcut">{shortcut}</TextLabel>
{/if}
</LayoutRow>
{/if}
{#if description}
<TextLabel class="tooltip-description">{description}</TextLabel>
{/if}
</FloatingMenu>
{/if}
</div>
<style lang="scss" global>
.tooltip {
position: absolute;
pointer-events: none;
width: 0;
height: 0;
.floating-menu-content {
max-width: Min(400px, 50vw);
.tooltip-header + .tooltip-description {
margin-top: 4px;
}
.text-label {
white-space: pre-wrap;
}
.text-label + .tooltip-shortcut {
margin-left: 8px;
}
.tooltip-shortcut {
color: var(--color-b-lightgray);
background: var(--color-3-darkgray);
padding: 0 4px;
border-radius: 4px;
}
.tooltip-description {
color: var(--color-b-lightgray);
}
}
}
</style>
@@ -1,5 +1,5 @@
<script lang="ts" context="module">
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
export type MenuType = "Popover" | "Tooltip" | "Dropdown" | "Dialog" | "Cursor";
/// Prevents the escape key from closing the parent floating menu of the given element.
/// This works by momentarily setting the `data-escape-does-not-close` attribute on the parent floating menu element.
@@ -64,7 +64,6 @@
let measuringOngoingGuard = false;
let minWidthParentWidth = 0;
let pointerStillDown = false;
let workspaceBounds = new DOMRect();
let floatingMenuBounds = new DOMRect();
let floatingMenuContentBounds = new DOMRect();
@@ -174,34 +173,50 @@
function positionAndStyleFloatingMenu() {
if (type === "Cursor") return;
const workspace = document.querySelector("[data-workspace]");
const floatingMenuContentDiv = floatingMenuContent?.div?.();
if (!workspace || !self || !floatingMenuContainer || !floatingMenuContent || !floatingMenuContentDiv) return;
if (!self || !floatingMenuContainer || !floatingMenuContent || !floatingMenuContentDiv) return;
const viewportBounds = document.documentElement.getBoundingClientRect();
workspaceBounds = workspace.getBoundingClientRect();
const windowBounds = document.documentElement.getBoundingClientRect();
floatingMenuBounds = self.getBoundingClientRect();
const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect();
floatingMenuContentBounds = floatingMenuContentDiv.getBoundingClientRect();
const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
const overflowingLeft = floatingMenuContentBounds.left - windowEdgeMargin <= windowBounds.left;
const overflowingRight = floatingMenuContentBounds.right + windowEdgeMargin >= windowBounds.right;
const overflowingTop = floatingMenuContentBounds.top - windowEdgeMargin <= windowBounds.top;
const overflowingBottom = floatingMenuContentBounds.bottom + windowEdgeMargin >= windowBounds.bottom;
// TODO: Make this work for all types. This is currently limited to tooltips because they're inherently small and transient.
// TODO: But on popovers and dropdowns, it's a bit harder to do this right. First we check if it's overflowing and flip the direction to avoid the overflow.
// TODO: But once it's flipped, if the position moves and the menu would no longer be overflowing, we're still flipped and thus unable to automatically notice the need to flip back.
// TODO: So as a result, once flipped, it stays flipped forever even if the menu spawner element is moved back away from the edge of the window.
if (type === "Tooltip") {
// Flip direction if overflowing the edge of the window
if (direction === "Top" && overflowingTop) direction = "Bottom";
else if (direction === "Bottom" && overflowingBottom) direction = "Top";
else if (direction === "Left" && overflowingLeft) direction = "Right";
else if (direction === "Right" && overflowingRight) direction = "Left";
}
const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
if (!inParentFloatingMenu) {
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
const tailOffset = type === "Popover" ? 10 : 0;
let tailOffset = 0;
if (type === "Popover") tailOffset = 10;
if (type === "Tooltip") tailOffset = direction === "Bottom" ? 20 : 10;
if (direction === "Bottom") floatingMenuContentDiv.style.top = `${tailOffset + floatingMenuBounds.y}px`;
if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + (viewportBounds.height - floatingMenuBounds.y)}px`;
if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + (windowBounds.height - floatingMenuBounds.y)}px`;
if (direction === "Right") floatingMenuContentDiv.style.left = `${tailOffset + floatingMenuBounds.x}px`;
if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + (viewportBounds.width - floatingMenuBounds.x)}px`;
if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + (windowBounds.width - floatingMenuBounds.x)}px`;
// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
if (tail && direction === "Bottom") tail.style.top = `${floatingMenuBounds.y}px`;
if (tail && direction === "Top") tail.style.bottom = `${viewportBounds.height - floatingMenuBounds.y}px`;
if (tail && direction === "Top") tail.style.bottom = `${windowBounds.height - floatingMenuBounds.y}px`;
if (tail && direction === "Right") tail.style.left = `${floatingMenuBounds.x}px`;
if (tail && direction === "Left") tail.style.right = `${viewportBounds.width - floatingMenuBounds.x}px`;
if (tail && direction === "Left") tail.style.right = `${windowBounds.width - floatingMenuBounds.x}px`;
}
type Edge = "Top" | "Bottom" | "Left" | "Right";
@@ -212,31 +227,31 @@
zeroedBorderVertical = direction === "Top" ? "Bottom" : "Top";
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
if (floatingMenuContentBounds.left - windowEdgeMargin <= workspaceBounds.left) {
if (overflowingLeft) {
floatingMenuContentDiv.style.left = `${windowEdgeMargin}px`;
if (workspaceBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left";
if (windowBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left";
}
if (floatingMenuContentBounds.right + windowEdgeMargin >= workspaceBounds.right) {
if (overflowingRight) {
floatingMenuContentDiv.style.right = `${windowEdgeMargin}px`;
if (workspaceBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right";
if (windowBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right";
}
}
if (direction === "Left" || direction === "Right") {
zeroedBorderHorizontal = direction === "Left" ? "Right" : "Left";
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
if (floatingMenuContentBounds.top - windowEdgeMargin <= workspaceBounds.top) {
if (overflowingTop) {
floatingMenuContentDiv.style.top = `${windowEdgeMargin}px`;
if (workspaceBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top";
if (windowBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top";
}
if (floatingMenuContentBounds.bottom + windowEdgeMargin >= workspaceBounds.bottom) {
if (overflowingBottom) {
floatingMenuContentDiv.style.bottom = `${windowEdgeMargin}px`;
if (workspaceBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom";
if (windowBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom";
}
}
// Remove the rounded corner from the content where the tail perfectly meets the corner
if (type === "Popover" && windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
if (displayTail && windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
case "TopLeft":
@@ -585,14 +600,18 @@
flex-direction: column;
}
&.top .tail {
&.top .tail,
&.topleft .tail,
&.topright .tail {
border-width: 8px 6px 0 6px;
border-color: var(--color-2-mildblack) transparent transparent transparent;
margin-left: -6px;
margin-bottom: 2px;
}
&.bottom .tail {
&.bottom .tail,
&.bottomleft .tail,
&.bottomright .tail {
border-width: 0 6px 8px 6px;
border-color: transparent transparent var(--color-2-mildblack) transparent;
margin-left: -6px;
@@ -5,7 +5,9 @@
let styleName = "";
export { styleName as style };
export let styles: Record<string, string | number | undefined> = {};
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// TODO: Add middle-click drag scrolling
export let scrollableX = false;
export let scrollableY = false;
@@ -26,13 +28,15 @@
<!-- Excluded events because these require `|passive` or `|nonpassive` modifiers. Use a <div> for these instead: `on:wheel`, `on:touchmove`, `on:touchstart` -->
<div
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-scrollable-x={scrollableX ? "" : undefined}
data-scrollable-y={scrollableY ? "" : undefined}
class={`layout-col ${className} ${extraClasses}`.trim()}
class:scrollable-x={scrollableX}
class:scrollable-y={scrollableY}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
bind:this={self}
on:auxclick
on:blur
@@ -5,7 +5,9 @@
let styleName = "";
export { styleName as style };
export let styles: Record<string, string | number | undefined> = {};
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// TODO: Add middle-click drag scrolling
export let scrollableX = false;
export let scrollableY = false;
@@ -26,13 +28,15 @@
<!-- Excluded events because these require `|passive` or `|nonpassive` modifiers. Use a <div> for these instead: `on:wheel`, `on:touchmove`, `on:touchstart` -->
<div
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-scrollable-x={scrollableX ? "" : undefined}
data-scrollable-y={scrollableY ? "" : undefined}
class={`layout-row ${className} ${extraClasses}`.trim()}
class:scrollable-x={scrollableX}
class:scrollable-y={scrollableY}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
bind:this={self}
on:auxclick
on:blur
@@ -698,7 +698,7 @@
.icon-button {
margin: 0;
&[title^="Coming Soon"] {
&[data-tooltip-description^="Coming soon."] {
opacity: 0.25;
transition: opacity 0.1s;
+16 -7
View File
@@ -609,7 +609,6 @@
styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }}
data-layer
data-index={index}
tooltip={listing.entry.tooltip}
on:pointerdown={(e) => layerPointerDown(e, listing)}
on:click={(e) => selectLayerWithModifiers(e, listing)}
>
@@ -618,9 +617,12 @@
class="expand-arrow"
class:expanded={listing.entry.expanded}
disabled={!listing.entry.childrenPresent}
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)" : ""}`}
data-tooltip-label={listing.entry.expanded ? "Collapse (All)" : "Expand (All)"}
data-tooltip-description={(listing.entry.expanded
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nNote: a selected layer is currently contained within.\n" : "")}
data-tooltip-shortcut="Alt Click"
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0"
></button>
@@ -628,7 +630,12 @@
<div class="expand-arrow-none"></div>
{/if}
{#if listing.entry.clipped}
<IconLabel icon="Clipped" class="clipped-arrow" tooltip="Clipping mask is active (Alt-click border to release)" />
<IconLabel
icon="Clipped"
class="clipped-arrow"
tooltipDescription="Clipping mask is active. To release it, perform the shortcut on the layer border."
tooltipShortcut="Alt Click"
/>
{/if}
<div class="thumbnail">
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
@@ -659,7 +666,8 @@
size={24}
icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"}
hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"}
tooltip={(listing.entry.unlocked ? "Lock" : "Unlock") + (!listing.entry.parentsUnlocked ? "\n(A parent of this layer is locked and that status is being inherited)" : "")}
tooltipLabel={listing.entry.unlocked ? "Lock" : "Unlock"}
tooltipDescription={!listing.entry.parentsUnlocked ? "A parent of this layer is locked and that status is being inherited." : ""}
/>
{/if}
<IconButton
@@ -669,7 +677,8 @@
size={24}
icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"}
hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"}
tooltip={(listing.entry.visible ? "Hide" : "Show") + (!listing.entry.parentsVisible ? "\n(A parent of this layer is hidden and that status is being inherited)" : "")}
tooltipLabel={listing.entry.visible ? "Hide" : "Show"}
tooltipDescription={!listing.entry.parentsVisible ? "A parent of this layer is hidden and that status is being inherited." : ""}
/>
</LayoutRow>
{/each}
+43 -25
View File
@@ -164,7 +164,7 @@
return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`;
}
function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string {
function dataTypeTooltipLabel(value: FrontendGraphInput | FrontendGraphOutput): string {
return `Data Type: ${value.resolvedType}`;
}
@@ -174,13 +174,11 @@
}
function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) return "Connected to nothing";
return `Connected to:\n${output.connectedTo.join("\n")}`;
return editor.handle.inDevelopmentMode() ? output.connectedTo.join("\n") : "";
}
function inputConnectedToText(input: FrontendGraphInput): string {
return `Connected to:\n${input.connectedTo}`;
return editor.handle.inDevelopmentMode() ? input.connectedTo : "";
}
function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
@@ -310,13 +308,14 @@
viewBox="0 0 8 8"
class="connector"
data-connector="output"
data-tooltip-label={dataTypeTooltipLabel(frontendOutput)}
data-tooltip-description={outputConnectedToText(frontendOutput)}
data-datatype={frontendOutput.dataType}
style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
>
<title>{`${dataTypeTooltip(frontendOutput)}\n\n${outputConnectedToText(frontendOutput)}`}</title>
{#if frontendOutput.connectedTo.length > 0}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -360,7 +359,7 @@
}}
/>
{#if index > 0}
<div class="reorder-drag-grip" title="Reorder this export" />
<div class="reorder-drag-grip" data-tooltip-description="Reorder this export" />
{/if}
{/if}
</div>
@@ -382,14 +381,15 @@
viewBox="0 0 8 8"
class="connector"
data-connector="input"
data-tooltip-label={dataTypeTooltipLabel(frontendInput)}
data-tooltip-description={inputConnectedToText(frontendInput)}
data-datatype={frontendInput.dataType}
style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
>
<title>{`${dataTypeTooltip(frontendInput)}\n\n${inputConnectedToText(frontendInput)}`}</title>
{#if frontendInput.connectedTo !== "nothing"}
{#if frontendInput.connectedTo !== "Connected to nothing."}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
@@ -406,7 +406,7 @@
>
{#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
{#if index > 0}
<div class="reorder-drag-grip" title="Reorder this export" />
<div class="reorder-drag-grip" data-tooltip-description="Reorder this export" />
{/if}
<IconButton
size={16}
@@ -505,7 +505,10 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}, Position: (${node.position.x}, ${node.position.y})` : "")}
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
`.trim()}
data-node={node.id}
>
<div class="thumbnail">
@@ -519,11 +522,12 @@
viewBox="0 0 8 12"
class="connector top"
data-connector="output"
data-tooltip-label={dataTypeTooltipLabel(node.primaryOutput)}
data-tooltip-description={outputConnectedToText(node.primaryOutput)}
data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
>
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#if node.primaryOutput.connectedTo.length > 0}
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
{#if node.primaryOutputConnectedToLayer}
@@ -540,14 +544,13 @@
viewBox="0 0 8 12"
class="connector bottom"
data-connector="input"
data-tooltip-label={node.primaryInput ? dataTypeTooltipLabel(node.primaryInput) : ""}
data-tooltip-description={node.primaryInput ? `${validTypesText(node.primaryInput).trim()}\n\n${inputConnectedToText(node.primaryInput)}` : ""}
data-datatype={node.primaryInput?.dataType}
style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`}
>
{#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
{/if}
{#if node.primaryInput?.connectedTo !== "nothing"}
{#if node.primaryInput?.connectedTo !== "Connected to nothing."}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
{#if node.primaryInputConnectedToLayer}
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
@@ -564,12 +567,13 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
data-tooltip-label={dataTypeTooltipLabel(stackDataInput)}
data-tooltip-description={`${validTypesText(stackDataInput).trim()}\n\n${inputConnectedToText(stackDataInput)}`}
data-connector="input"
data-datatype={stackDataInput.dataType}
style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
>
<title>{`${dataTypeTooltip(stackDataInput)}\n\n${validTypesText(stackDataInput)}\n\n${inputConnectedToText(stackDataInput)}`}</title>
{#if stackDataInput.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -582,7 +586,7 @@
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel>{node.displayName}</TextLabel>
</div>
<div class="solo-drag-grip" title="Drag only this layer without pushing others outside the stack"></div>
<div class="solo-drag-grip" data-tooltip-description="Drag only this layer without pushing others outside the stack"></div>
<IconButton
class="visibility"
data-visibility-button
@@ -591,7 +595,7 @@
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
tooltip={node.visible ? "Visible" : "Hidden"}
tooltipLabel={node.visible ? "Visible" : "Hidden"}
/>
<svg class="border-mask" width="0" height="0">
@@ -650,7 +654,10 @@
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}, Position: (${node.position.x}, ${node.position.y})` : "")}
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
`.trim()}
data-node={node.id}
>
<!-- Primary row -->
@@ -664,7 +671,7 @@
<div class="secondary" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
{#each exposedInputsOutputs as [input, output]}
<div class={`secondary-row expanded ${input !== undefined ? "input" : "output"}`}>
<TextLabel tooltip={(input !== undefined ? `${input.name}\n\n${input.description}` : `${output.name}\n\n${output.description}`).trim()}>
<TextLabel tooltipLabel={input !== undefined ? input.name : output.name} tooltipDescription={input !== undefined ? input.description : output.description}>
{input !== undefined ? input.name : output.name}
</TextLabel>
</div>
@@ -679,11 +686,12 @@
viewBox="0 0 8 8"
class="connector primary-connector"
data-connector="input"
data-tooltip-label={dataTypeTooltipLabel(node.primaryInput)}
data-tooltip-description={`${validTypesText(node.primaryInput).trim()}\n\n${inputConnectedToText(node.primaryInput)}`}
data-datatype={node.primaryInput?.dataType}
style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
>
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
{#if node.primaryInput.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -698,11 +706,12 @@
viewBox="0 0 8 8"
class="connector"
data-connector="input"
data-tooltip-label={dataTypeTooltipLabel(secondary)}
data-tooltip-description={`${validTypesText(secondary).trim()}\n\n${inputConnectedToText(secondary)}`}
data-datatype={secondary.dataType}
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
>
<title>{`${dataTypeTooltip(secondary)}\n\n${validTypesText(secondary)}\n\n${inputConnectedToText(secondary)}`}</title>
{#if secondary.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -720,11 +729,12 @@
viewBox="0 0 8 8"
class="connector primary-connector"
data-connector="output"
data-tooltip-label={dataTypeTooltipLabel(node.primaryOutput)}
data-tooltip-description={`${outputConnectedToText(node.primaryOutput)}`}
data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
>
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#if node.primaryOutput.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -738,11 +748,12 @@
viewBox="0 0 8 8"
class="connector"
data-connector="output"
data-tooltip-label={dataTypeTooltipLabel(secondary)}
data-tooltip-description={`${outputConnectedToText(secondary)}`}
data-datatype={secondary.dataType}
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
>
<title>{`${dataTypeTooltip(secondary)}\n\n${outputConnectedToText(secondary)}`}</title>
{#if secondary.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -957,6 +968,7 @@
.imports-and-exports {
width: 100%;
height: 100%;
pointer-events: none;
position: absolute;
// Keeps the connectors above the wires
z-index: 1;
@@ -1054,8 +1066,14 @@
.layers-and-nodes {
position: absolute;
pointer-events: none;
width: 100%;
height: 100%;
// Zero specificity with `:where()` to allow other rules to override `pointer-events`
:where(& > *) {
pointer-events: auto;
}
}
.layer,
@@ -26,10 +26,10 @@
<LayoutCol class={`widget-section ${className}`.trim()} {classes}>
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
<div class="expand-arrow" />
<TextLabel tooltip={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
<TextLabel tooltipLabel={widgetData.name} tooltipDescription={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
<IconButton
icon={widgetData.pinned ? "PinActive" : "PinInactive"}
tooltip={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected" : "Pin this node so it's shown here when nothing is selected"}
tooltipDescription={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected." : "Pin this node so it's shown here when nothing is selected."}
size={24}
action={(e) => {
editor.handle.setNodePinned(widgetData.id, !widgetData.pinned);
@@ -39,7 +39,7 @@
/>
<IconButton
icon="Trash"
tooltip="Delete this node from the layer chain"
tooltipDescription="Delete this node from the layer chain."
size={24}
action={(e) => {
editor.handle.deleteNode(widgetData.id);
@@ -50,7 +50,7 @@
<IconButton
icon={widgetData.visible ? "EyeVisible" : "EyeHidden"}
hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"}
tooltip={widgetData.visible ? "Hide this node" : "Show this node"}
tooltipDescription={widgetData.visible ? "Hide this node." : "Show this node."}
size={24}
action={(e) => {
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
@@ -4,12 +4,14 @@
export let labels: string[];
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (index: number) => void;
</script>
<LayoutRow class="breadcrumb-trail-buttons" {tooltip}>
<LayoutRow class="breadcrumb-trail-buttons" {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{#each labels as label, index}
<TextButton {label} emphasized={index === labels.length - 1} {disabled} action={() => !disabled && index !== labels.length - 1 && action(index)} />
{/each}
@@ -8,7 +8,9 @@
export let size: IconSize;
export let disabled = false;
export let active = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
@@ -28,7 +30,9 @@
class:active
on:click={action}
{disabled}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
tabindex={active ? -1 : 0}
{...$$restProps}
>
@@ -8,7 +8,9 @@
export let image: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
@@ -17,7 +19,17 @@
.join(" ");
</script>
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-button ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} />
<img
src={IMAGE_BASE64_STRINGS[image]}
style:width
style:height
class={`image-button ${className} ${extraClasses}`.trim()}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
alt=""
on:click={action}
/>
<style lang="scss" global>
.image-button {
@@ -5,7 +5,9 @@
export let exposed: boolean;
export let dataType: FrontendGraphDataType;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
</script>
@@ -16,7 +18,9 @@
style:--data-type-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-type-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
on:click={action}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
tabindex="-1"
>
{#if !exposed}
@@ -11,7 +11,9 @@
export let style: PopoverButtonStyle = "DropdownArrow";
export let menuDirection: MenuDirection = "Bottom";
export let icon: IconName | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let disabled = false;
export let popoverMinWidth = 1;
@@ -27,9 +29,20 @@
</script>
<LayoutRow class="popover-button" classes={{ "has-icon": icon !== undefined, "direction-top": menuDirection === "Top" }}>
<IconButton class="dropdown-icon" classes={{ open }} {disabled} action={() => onClick()} icon={style || "DropdownArrow"} size={16} {tooltip} data-floating-menu-spawner />
<IconButton
class="dropdown-icon"
classes={{ open }}
{disabled}
action={() => onClick()}
icon={style || "DropdownArrow"}
size={16}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
data-floating-menu-spawner
/>
{#if icon !== undefined}
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} />
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltipLabel} {tooltipDescription} {tooltipShortcut} />
{/if}
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}>
@@ -20,7 +20,9 @@
export let minWidth = 0;
export let disabled = false;
export let narrow = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
// Callbacks
@@ -59,7 +61,9 @@
class:narrow
class:flush
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-emphasized={emphasized || undefined}
data-disabled={disabled || undefined}
data-text-button
@@ -11,7 +11,9 @@
export let checked = false;
export let disabled = false;
export let icon: IconName = "Checkmark";
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let forLabel: bigint | undefined = undefined;
let inputElement: HTMLInputElement | undefined;
@@ -46,7 +48,15 @@
tabindex={disabled ? -1 : 0}
bind:this={inputElement}
/>
<label class:disabled class:checked for={`checkbox-input-${id}`} on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)} title={tooltip}>
<label
class:disabled
class:checked
for={`checkbox-input-${id}`}
on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
>
<LayoutRow class="checkbox-box">
<IconLabel icon={displayIcon} />
</LayoutRow>
@@ -17,7 +17,9 @@
export let allowNone = false;
export let menuDirection: MenuDirection = "Bottom";
// export let allowTransparency = false; // TODO: Implement
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
$: outlined = outlineFactor > 0.0001;
@@ -26,7 +28,7 @@
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
</script>
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
<!-- {#if disabled && value instanceof Color && !value.none}
<TextLabel>sRGB</TextLabel>
@@ -16,7 +16,9 @@
export let styles: Record<string, string | number | undefined> = {};
export let value: Curve;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
const GRID_SIZE = 4;
@@ -77,7 +79,7 @@
}
function handleManipulatorPointerDown(e: PointerEvent, i: number) {
// Delete an anchor with RMB or MMB
// Delete an anchor with right click or middle click
if (e.button > 0 && i > 0 && i < manipulatorsList.length - 1) {
draggedNodeIndex = undefined;
selectedNodeIndex = undefined;
@@ -188,7 +190,7 @@
}
</script>
<LayoutRow class="curve-input" classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}>
<LayoutRow class="curve-input" classes={{ disabled, ...classes }} style={styleName} {styles} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
<svg viewBox="0 0 1 1" on:pointermove={handlePointerMove} on:pointerup={handlePointerUp}>
{#each { length: GRID_SIZE - 1 } as _, i}
<path class="grid" d={`M 0 ${(i + 1) / GRID_SIZE} L 1 ${(i + 1) / GRID_SIZE}`} />
@@ -21,7 +21,9 @@
export let interactive = true;
export let disabled = false;
export let narrow = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let minWidth = 0;
export let maxWidth = 0;
@@ -98,7 +100,9 @@
<LayoutRow
class="dropdown-box"
classes={{ disabled, open }}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
on:click={() => !disabled && (open = true)}
on:blur={unFocusDropdownBox}
tabindex={disabled ? -1 : 0}
@@ -25,7 +25,9 @@
export let disabled = false;
export let narrow = false;
export let textarea = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let hideContextMenu = false;
@@ -74,7 +76,7 @@
</script>
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
<LayoutRow class={`field-input ${className}`} classes={{ disabled, narrow, ...classes }} style={styleName} {styles} {tooltip}>
<LayoutRow class={`field-input ${className}`} classes={{ disabled, narrow, ...classes }} style={styleName} {styles} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{#if !textarea}
<input
type="text"
@@ -23,7 +23,9 @@
export let fontStyle: string;
export let isStyle = false;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
let open = false;
let entries: MenuListEntry[] = [];
@@ -108,7 +110,9 @@
class="dropdown-box"
classes={{ disabled }}
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
tabindex={disabled ? -1 : 0}
on:click={toggleOpen}
data-floating-menu-spawner
@@ -18,7 +18,9 @@
// Label
export let label: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Disabled
export let disabled = false;
@@ -688,7 +690,9 @@
{label}
{disabled}
{narrow}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
{styles}
hideContextMenu={true}
spellcheck={false}
@@ -27,7 +27,15 @@
<LayoutRow class="radio-input" classes={{ disabled, narrow, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}>
{#each entries as entry, index}
<button class:active={!mixed ? index === selectedIndex : undefined} on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
<button
class:active={!mixed ? index === selectedIndex : undefined}
on:click={() => handleEntryClick(entry)}
data-tooltip-label={entry.tooltipLabel}
data-tooltip-description={entry.tooltipDescription}
data-tooltip-shortcut={entry.tooltipShortcut}
tabindex={index === selectedIndex ? -1 : 0}
{disabled}
>
{#if entry.icon}
<IconLabel icon={entry.icon} />
{/if}
@@ -7,14 +7,16 @@
export let value: string;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
function setValue(newValue: ReferencePoint) {
dispatch("value", newValue);
}
</script>
<div class="reference-point-input" class:disabled title={tooltip}>
<div class="reference-point-input" class:disabled data-tooltip-label={tooltipLabel} data-tooltip-description={tooltipDescription} data-tooltip-shortcut={tooltipShortcut}>
<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>
@@ -16,7 +16,9 @@
export let disabled = false;
export let activeMarkerIndex = 0 as number | undefined;
// export let disabled = false;
// export let tooltip: string | undefined = undefined;
// export let tooltipLabel: string | undefined = undefined;
// export let tooltipDescription: string | undefined = undefined;
// export let tooltipShortcut: string | undefined = undefined;
let markerTrack: LayoutRow | undefined = undefined;
let positionRestore: number | undefined = undefined;
@@ -7,7 +7,9 @@
export let value: string;
export let label: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let disabled = false;
let self: FieldInput | undefined;
@@ -55,7 +57,9 @@
spellcheck={true}
{label}
{disabled}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
bind:this={self}
/>
@@ -7,7 +7,9 @@
// Label
export let label: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
// Disabled
export let disabled = false;
@@ -79,7 +81,9 @@
{label}
{disabled}
{narrow}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
{placeholder}
bind:this={self}
/>
@@ -9,7 +9,9 @@
export let icon: IconName;
export let iconSizeOverride: number | undefined = undefined;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
$: iconSizeClass = ((icon: IconName) => {
const iconData = ICONS[icon];
@@ -26,7 +28,7 @@
.join(" ");
</script>
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{@html ICON_SVG_STRINGS[icon] || ""}
</LayoutRow>
@@ -6,14 +6,25 @@
export let url: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
$: extraClasses = Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" ");
</script>
<img src={url} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" />
<img
src={url}
style:width
style:height
class={`image-label ${className} ${extraClasses}`.trim()}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
alt=""
/>
<style lang="scss" global>
.image-label {
@@ -14,7 +14,9 @@
export let tableAlign = false;
export let minWidth = "";
export let multiline = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let forCheckbox: bigint | undefined = undefined;
$: extraClasses = Object.entries(classes)
@@ -37,7 +39,9 @@
class:table-align={tableAlign}
style:min-width={minWidth || undefined}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined}
>
<slot />
@@ -45,7 +45,7 @@
$: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked;
function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string {
const RESERVED = "This hotkey is reserved by the browser. ";
const RESERVED = "This keyboard shortcut is reserved by the browser.";
const USE_FULLSCREEN = "It is made available in fullscreen mode.";
const USE_SECURE_CTX = "It is made available in fullscreen mode when this website is served from a secure context (https or localhost).";
const SWITCH_BROWSER = "Use a Chromium-based browser (like Chrome or Edge) in fullscreen mode to directly use the shortcut.";
@@ -118,7 +118,7 @@
</script>
{#if displayKeyboardLockNotice}
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltip={keyboardLockInfoMessage} />
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltipDescription={keyboardLockInfoMessage} />
{:else}
<LayoutRow class="user-input-label" classes={{ "text-only": textOnly }}>
{#each keysWithLabelsGroups as keysWithLabels, groupIndex}
@@ -189,11 +189,13 @@
font-weight: 400;
text-align: center;
height: 16px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-5-dullgray);
color: var(--color-e-nearwhite);
background: var(--color-3-darkgray);
color: var(--color-b-lightgray);
.icon-label {
fill: var(--color-b-lightgray);
}
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
@@ -2,21 +2,31 @@
import { getContext } from "svelte";
import type { AppWindowState } from "@graphite/state-providers/app-window";
import type { DialogState } from "@graphite/state-providers/dialog";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import Dialog from "@graphite/components/floating-menus/Dialog.svelte";
import Tooltip from "@graphite/components/floating-menus/Tooltip.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import StatusBar from "@graphite/components/window/status-bar/StatusBar.svelte";
import TitleBar from "@graphite/components/window/title-bar/TitleBar.svelte";
import Workspace from "@graphite/components/window/workspace/Workspace.svelte";
const dialog = getContext<DialogState>("dialog");
const tooltip = getContext<TooltipState>("tooltip");
const appWindow = getContext<AppWindowState>("appWindow");
</script>
<LayoutCol class="main-window" classes={{ "viewport-hole-punch": $appWindow.viewportHolePunch }}>
<TitleBar />
<Workspace />
<StatusBar />
{#if $dialog.visible}
<Dialog />
{/if}
{#if $tooltip.visible}
<Tooltip />
{/if}
</LayoutCol>
<style lang="scss" global>
@@ -11,13 +11,13 @@
const editor = getContext<Editor>("editor");
</script>
<LayoutRow class="window-button linux" tooltip="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
<LayoutRow class="window-button linux" tooltipLabel="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
<IconLabel icon="WindowButtonWinMinimize" />
</LayoutRow>
<LayoutRow class="window-button linux" tooltip={$appWindow.maximized ? "Unmaximize" : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
<LayoutRow class="window-button linux" tooltipLabel={$appWindow.maximized ? "Unmaximize" : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
<IconLabel icon={$appWindow.maximized ? "WindowButtonWinRestoreDown" : "WindowButtonWinMaximize"} />
</LayoutRow>
<LayoutRow class="window-button linux" tooltip="Close" on:click={() => editor.handle.appWindowClose()}>
<LayoutRow class="window-button linux" tooltipLabel="Close" on:click={() => editor.handle.appWindowClose()}>
<IconLabel icon="WindowButtonWinClose" />
</LayoutRow>
@@ -8,8 +8,6 @@
const fullscreen = getContext<FullscreenState>("fullscreen");
$: requestFullscreenHotkeys = $fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked;
async function handleClick() {
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
else fullscreen.enterFullscreen();
@@ -19,8 +17,9 @@
<LayoutRow
class="window-buttons-web"
on:click={handleClick}
tooltip={($fullscreen.windowFullscreen ? "Exit Fullscreen (F11)" : "Enter Fullscreen (F11)") +
(requestFullscreenHotkeys ? "\n\nThis provides access to hotkeys normally reserved by the browser" : "")}
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
tooltipDescription={$fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : ""}
tooltipShortcut="F11"
>
<IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
</LayoutRow>
@@ -11,13 +11,13 @@
const editor = getContext<Editor>("editor");
</script>
<LayoutRow class="window-button windows" tooltip="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
<LayoutRow class="window-button windows" tooltipLabel="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
<IconLabel icon="WindowButtonWinMinimize" />
</LayoutRow>
<LayoutRow class="window-button windows" tooltip={$appWindow.maximized ? "Restore Down" : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
<LayoutRow class="window-button windows" tooltipLabel={$appWindow.maximized ? "Restore Down" : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
<IconLabel icon={$appWindow.maximized ? "WindowButtonWinRestoreDown" : "WindowButtonWinMaximize"} />
</LayoutRow>
<LayoutRow class="window-button windows" tooltip="Close" on:click={() => editor.handle.appWindowClose()}>
<LayoutRow class="window-button windows" tooltipLabel="Close" on:click={() => editor.handle.appWindowClose()}>
<IconLabel icon="WindowButtonWinClose" />
</LayoutRow>
@@ -38,7 +38,7 @@
export let tabMinWidths = false;
export let tabCloseButtons = false;
export let tabLabels: { name: string; unsaved?: boolean; tooltip?: string }[];
export let tabLabels: { name: string; unsaved?: boolean; tooltipDescription?: string; tooltipShortcut?: string }[];
export let tabActiveIndex: number;
export let panelType: PanelType | undefined = undefined;
export let clickAction: ((index: number) => void) | undefined = undefined;
@@ -118,7 +118,8 @@
<LayoutRow
class="tab"
classes={{ active: tabIndex === tabActiveIndex }}
tooltip={tabLabel.tooltip || undefined}
tooltipLabel={tabLabel.name}
tooltipDescription={tabLabel.tooltipDescription}
on:click={(e) => {
e.stopPropagation();
clickAction?.(tabIndex);
@@ -3,10 +3,8 @@
import type { Editor } from "@graphite/editor";
import type { OpenDocument } from "@graphite/messages";
import type { DialogState } from "@graphite/state-providers/dialog";
import type { PortfolioState } from "@graphite/state-providers/portfolio";
import Dialog from "@graphite/components/floating-menus/Dialog.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Panel from "@graphite/components/window/workspace/Panel.svelte";
@@ -34,13 +32,12 @@
const unsaved = !doc.details.isSaved;
if (!editor.handle.inDevelopmentMode()) return { name, unsaved };
const tooltip = `Document ID: ${doc.id}`;
return { name, unsaved, tooltip };
const tooltipDescription = `Document ID: ${doc.id}`;
return { name, unsaved, tooltipDescription };
});
const editor = getContext<Editor>("editor");
const portfolio = getContext<PortfolioState>("portfolio");
const dialog = getContext<DialogState>("dialog");
function resizePanel(e: PointerEvent) {
const gutter = (e.target || undefined) as HTMLDivElement | undefined;
@@ -175,9 +172,6 @@
</LayoutCol>
{/if}
</LayoutRow>
{#if $dialog.visible}
<Dialog />
{/if}
</LayoutRow>
<style lang="scss" global>
+137 -24
View File
@@ -335,7 +335,7 @@ export type Key = { key: KeyRaw; label: string };
export type LayoutKeysGroup = Key[];
export type ActionKeys = { keys: LayoutKeysGroup };
export type MouseMotion = string;
export type MouseMotion = "None" | "Lmb" | "Rmb" | "Mmb" | "ScrollUp" | "ScrollDown" | "Drag" | "LmbDouble" | "LmbDrag" | "RmbDrag" | "RmbDouble" | "MmbDrag";
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
export type HSVA = { h: number; s: number; v: number; a: number };
@@ -828,7 +828,7 @@ export class LayerPanelEntry {
alias!: string;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
debugLayerIdTooltip!: string | undefined;
inSelectedNetwork!: boolean;
@@ -905,7 +905,13 @@ export class CheckboxInput extends WidgetProps {
icon!: IconName;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
forLabel!: bigint | undefined;
}
@@ -943,7 +949,13 @@ export class ColorInput extends WidgetProps {
// allowTransparency!: boolean; // TODO: Implement
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export type FillChoice = Color | Gradient;
@@ -1000,7 +1012,8 @@ export type MenuListEntry = MenuEntryCommon & {
value: string;
shortcutRequiresLock?: boolean;
disabled?: boolean;
tooltip?: string;
tooltipLabel?: string;
tooltipDescription?: string;
font?: URL;
};
@@ -1021,7 +1034,13 @@ export class CurveInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class DropdownInput extends WidgetProps {
@@ -1038,7 +1057,13 @@ export class DropdownInput extends WidgetProps {
narrow!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
// Styling
@@ -1057,7 +1082,13 @@ export class FontInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class IconButton extends WidgetProps {
@@ -1072,7 +1103,13 @@ export class IconButton extends WidgetProps {
active!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class IconLabel extends WidgetProps {
@@ -1081,7 +1118,13 @@ export class IconLabel extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class ImageButton extends WidgetProps {
@@ -1094,7 +1137,13 @@ export class ImageButton extends WidgetProps {
height!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class ImageLabel extends WidgetProps {
@@ -1107,7 +1156,13 @@ export class ImageLabel extends WidgetProps {
height!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
@@ -1119,7 +1174,13 @@ export class NumberInput extends WidgetProps {
label!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
// Disabled
@@ -1179,7 +1240,13 @@ export class PopoverButton extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
// Body
popoverLayout!: LayoutGroup[];
@@ -1193,7 +1260,9 @@ export type RadioEntryData = {
value?: string;
label?: string;
icon?: IconName;
tooltip?: string;
tooltipLabel?: string;
tooltipDescription?: string;
tooltipShortcut?: string;
// Callbacks
action?: () => void;
@@ -1237,7 +1306,13 @@ export class TextAreaInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class ParameterExposeButton extends WidgetProps {
@@ -1246,7 +1321,13 @@ export class ParameterExposeButton extends WidgetProps {
dataType!: FrontendGraphDataType;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class TextButton extends WidgetProps {
@@ -1267,13 +1348,20 @@ export class TextButton extends WidgetProps {
narrow!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
menuListChildren!: MenuListEntry[][];
}
export type TextButtonWidget = {
tooltip?: string;
tooltipLabel?: string;
tooltipDescription?: string;
message?: string | object;
callback?: () => void;
props: {
@@ -1284,7 +1372,8 @@ export type TextButtonWidget = {
flush?: boolean;
minWidth?: number;
disabled?: boolean;
tooltip?: string;
tooltipLabel?: string;
tooltipDescription?: string;
// Callbacks
// `action` is used via `IconButtonWidget.callback`
@@ -1297,7 +1386,13 @@ export class BreadcrumbTrailButtons extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class TextInput extends WidgetProps {
@@ -1314,7 +1409,13 @@ export class TextInput extends WidgetProps {
maxWidth!: number;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
export class TextLabel extends WidgetProps {
@@ -1341,7 +1442,13 @@ export class TextLabel extends WidgetProps {
minWidth!: string;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
forCheckbox!: bigint | undefined;
}
@@ -1354,7 +1461,13 @@ export class ReferencePointInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
tooltipLabel!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipDescription!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltipShortcut!: string | undefined;
}
// WIDGET
+64
View File
@@ -0,0 +1,64 @@
import { writable } from "svelte/store";
const SHOW_TOOLTIP_DELAY_MS = 500;
export function createTooltipState() {
const { subscribe, update } = writable({
visible: false,
element: undefined as Element | undefined,
position: { x: 0, y: 0 },
});
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
// Listen for mouse movements onto tooltip-bearing HTML elements to track the future target of a tooltip
document.addEventListener("mouseover", (e) => {
const element = (e.target instanceof Element && e.target.closest("[data-tooltip-label], [data-tooltip-description], [data-tooltip-shortcut]")) || undefined;
update((state) => {
state.visible = false;
state.element = element;
return state;
});
});
// Listen for mouse movements to schedule and position the tooltip, or hide it immediately upon further movement
document.addEventListener("mousemove", (e) => {
// Hide the tooltip now that the cursor has moved
update((state) => {
state.visible = false;
return state;
});
// Before we schedule a new future tooltip appearance, we clear the existing one
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// Schedule the tooltip to appear at this cursor position after a delay
tooltipTimeout = setTimeout(() => {
update((state) => {
if (state.element) {
state.visible = true;
state.position = { x: e.clientX, y: e.clientY };
}
return state;
});
}, SHOW_TOOLTIP_DELAY_MS);
});
document.addEventListener("mousedown", closeTooltip);
document.addEventListener("keydown", closeTooltip);
// Stop showing a tooltip if the user clicks or presses a key, and require the user to first move out of the element before it can re-appear
function closeTooltip() {
update((state) => {
state.visible = false;
state.element = undefined;
return state;
});
}
return {
subscribe,
};
}
export type TooltipState = ReturnType<typeof createTooltipState>;