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
@@ -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 0–255">RGB</TextLabel>
<TextLabel tooltipLabel="Red/Green/Blue" tooltipDescription="Integers 0–255.">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 0–255`}
tooltipLabel={{ r: "Red Channel", g: "Green Channel", b: "Blue Channel" }[channel]}
tooltipDescription="Integers 0–255."
/>
{/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>