Replace the Spreadsheet panel with an improved Data panel (#3037)

* Improve the table data panel

* Add the "Window" menu bar section and polish everything
This commit is contained in:
Keavon Chambers
2025-08-10 07:46:42 -07:00
committed by GitHub
parent 2f4aef34e5
commit 2bb4509647
54 changed files with 1300 additions and 645 deletions
@@ -39,6 +39,7 @@
export let colorOrGradient: FillChoice;
export let allowNone = false;
// export let allowTransparency = false; // TODO: Implement
export let disabled = false;
export let direction: MenuDirection = "Bottom";
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
export let open: boolean;
@@ -133,6 +134,8 @@
}
function onPointerDown(e: PointerEvent) {
if (disabled) return;
const target = (e.target || undefined) as HTMLElement | undefined;
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
@@ -403,7 +406,7 @@
});
</script>
<FloatingMenu class="color-picker" {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
<FloatingMenu class="color-picker" classes={{ disabled }} {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
<LayoutRow
styles={{
"--new-color": newColor.toHexOptionalAlpha(),
@@ -418,7 +421,7 @@
>
<LayoutCol class="pickers-and-gradient">
<LayoutRow class="pickers">
<LayoutCol class="saturation-value-picker" on:pointerdown={onPointerDown} data-saturation-value-picker>
<LayoutCol class="saturation-value-picker" title={disabled ? "Saturation and value (disabled)" : "Saturation and value"} on:pointerdown={onPointerDown} data-saturation-value-picker>
{#if !isNone}
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
{/if}
@@ -432,12 +435,12 @@
/>
{/if}
</LayoutCol>
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
<LayoutCol class="hue-picker" title={disabled ? "Hue (disabled)" : "Hue"} on:pointerdown={onPointerDown} data-hue-picker>
{#if !isNone}
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
{/if}
</LayoutCol>
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
<LayoutCol class="alpha-picker" title={disabled ? "Alpha (disabled)" : "Alpha"} on:pointerdown={onPointerDown} data-alpha-picker>
{#if !isNone}
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
{/if}
@@ -447,6 +450,7 @@
<LayoutRow class="gradient">
<SpectrumInput
{gradient}
{disabled}
on:gradient={() => {
gradient = gradient;
if (gradient) dispatch("colorOrGradient", gradient);
@@ -459,6 +463,7 @@
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
<NumberInput
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
{disabled}
on:value={({ detail }) => {
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
}}
@@ -478,7 +483,7 @@
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"}
>
{#if !newColor.equals(oldColor)}
{#if !newColor.equals(oldColor) && !disabled}
<div class="swap-button-background"></div>
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
{/if}
@@ -500,6 +505,7 @@
<LayoutRow>
<TextInput
value={newColor.toHexOptionalAlpha() || "-"}
{disabled}
on:commitText={({ detail }) => {
dispatch("startHistoryTransaction");
setColorCode(detail);
@@ -520,6 +526,7 @@
{/if}
<NumberInput
value={strength}
{disabled}
on:value={({ detail }) => {
strength = detail;
setColorRGB(channel, detail);
@@ -547,6 +554,7 @@
{/if}
<NumberInput
value={strength}
{disabled}
on:value={({ detail }) => {
strength = detail;
setColorHSV(channel, detail);
@@ -573,6 +581,7 @@
<Separator type="Related" />
<NumberInput
value={!isNone ? alpha * 100 : undefined}
{disabled}
on:value={({ detail }) => {
if (detail !== undefined) alpha = detail / 100;
setColorAlphaPercent(detail);
@@ -593,14 +602,14 @@
<LayoutRow class="leftover-space" />
<LayoutRow>
{#if allowNone && !gradient}
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
<button class="preset-color none" {disabled} on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
<Separator type="Related" />
{/if}
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
<button class="preset-color black" {disabled} on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
<Separator type="Related" />
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
<button class="preset-color white" {disabled} on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
<Separator type="Related" />
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
<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" />
@@ -609,7 +618,7 @@
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
</button>
<Separator type="Related" />
<IconButton icon="Eyedropper" size={24} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
<IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
</LayoutRow>
</LayoutCol>
</LayoutRow>
@@ -954,7 +963,7 @@
// For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors.
// We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover.
background: var(--pure-color-gray);
transition: background-color 0.2s ease;
transition: background-color 0.1s;
}
&:hover div {
@@ -963,5 +972,21 @@
}
}
}
&.disabled .pickers-and-gradient .pickers :is(.saturation-value-picker, .hue-picker, .alpha-picker),
&.disabled .details .preset-color,
&.disabled .details .choice-preview {
transition: opacity 0.1s;
&:hover {
opacity: 0.5;
}
}
&.disabled .details .preset-color.pure:hover div {
background: var(--pure-color-gray);
}
}
// paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding
</style>
@@ -0,0 +1,50 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdateDataPanelLayout } from "@graphite/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
let dataPanelLayout = defaultWidgetLayout();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateDataPanelLayout, (updateDataPanelLayout) => {
patchWidgetLayout(dataPanelLayout, updateDataPanelLayout);
dataPanelLayout = dataPanelLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateDataPanelLayout);
});
</script>
<LayoutCol class="data-panel">
<LayoutCol class="body" scrollableY={true}>
<WidgetLayout layout={dataPanelLayout} />
</LayoutCol>
</LayoutCol>
<style lang="scss" global>
.data-panel {
flex-grow: 1;
padding: 4px;
table {
margin: -4px;
width: calc(100% + 2 * 4px);
.text-label {
white-space: wrap;
}
&:not(:first-child) {
margin-top: 0;
}
}
}
</style>
@@ -675,7 +675,7 @@
&[title^="Coming Soon"] {
opacity: 0.25;
transition: opacity 0.2s;
transition: opacity 0.1s;
&:hover {
opacity: 1;
@@ -825,7 +825,7 @@
.graph-view {
pointer-events: none;
transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s;
opacity: 0;
&.open {
+17 -1
View File
@@ -98,6 +98,12 @@
});
onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout);
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout);
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout);
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs);
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails);
removeEventListener("pointermove", clippingHover);
removeEventListener("keydown", clippingKeyPress);
removeEventListener("keyup", clippingKeyPress);
@@ -489,7 +495,9 @@
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
<LayoutRow class="control-bar" scrollableX={true}>
<WidgetLayout layout={layersPanelControlBarLeftLayout} />
<Separator />
{#if layersPanelControlBarLeftLayout?.layout?.length > 0 && layersPanelControlBarRightLayout?.layout?.length > 0}
<Separator />
{/if}
<WidgetLayout layout={layersPanelControlBarRightLayout} />
</LayoutRow>
<LayoutRow class="list-area" scrollableY={true}>
@@ -605,6 +613,10 @@
.widget-span:first-child {
flex: 1 1 auto;
}
&:not(:has(*)) {
display: none;
}
}
// Bottom bar
@@ -619,6 +631,10 @@
.widget-span > * {
margin: 0;
}
&:not(:has(*)) {
display: none;
}
}
// Layer hierarchy
@@ -1,27 +1,31 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages";
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertiesPanelLayout } from "@graphite/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
let propertiesSectionsLayout = defaultWidgetLayout();
let propertiesPanelLayout = defaultWidgetLayout();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
propertiesSectionsLayout = propertiesSectionsLayout;
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelLayout, (updatePropertiesPanelLayout) => {
patchWidgetLayout(propertiesPanelLayout, updatePropertiesPanelLayout);
propertiesPanelLayout = propertiesPanelLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdatePropertiesPanelLayout);
});
</script>
<LayoutCol class="properties">
<LayoutCol class="sections" scrollableY={true}>
<WidgetLayout layout={propertiesSectionsLayout} />
<WidgetLayout layout={propertiesPanelLayout} />
</LayoutCol>
</LayoutCol>
@@ -1,50 +0,0 @@
<script lang="ts">
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import { getContext } from "svelte";
import type { PortfolioState } from "/src/state-providers/portfolio";
const portfolio = getContext<PortfolioState>("portfolio");
</script>
<LayoutCol class="spreadsheet">
<LayoutRow class="control-bar">
<TextLabel>Spreadsheet Data for Node ID {$portfolio.spreadsheetNode}:</TextLabel>
</LayoutRow>
<LayoutCol class="body" scrollableY={true}>
<WidgetLayout layout={$portfolio.spreadsheetWidgets} />
</LayoutCol>
</LayoutCol>
<style lang="scss" global>
.spreadsheet {
flex-grow: 1;
padding: 4px;
.control-bar {
height: 32px;
--widget-height: 24px;
flex: 0 0 auto;
.text-label {
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
min-height: var(--widget-height);
line-height: var(--widget-height);
}
}
table {
margin: 0 -4px;
width: calc(100% + 2 * 4px);
margin-top: 8px;
.text-label {
white-space: wrap;
}
}
}
</style>
+2 -2
View File
@@ -1008,7 +1008,7 @@
border-radius: 4px;
bottom: calc(100% + 12px);
z-index: -1;
transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s;
opacity: 0.5;
// Tail
@@ -1040,7 +1040,7 @@
-webkit-user-select: text;
user-select: text;
transition:
opacity 0.2s ease-in-out,
opacity 0.2s,
z-index 0s 0.2s;
&::selection {
@@ -25,6 +25,7 @@
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import ImageLabel from "@graphite/components/widgets/labels/ImageLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
@@ -124,6 +125,10 @@
{#if iconLabel}
<IconLabel {...exclude(iconLabel)} />
{/if}
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
{#if imageLabel}
<ImageLabel {...exclude(imageLabel)} />
{/if}
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
{#if imageButton}
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
@@ -17,14 +17,15 @@
.join(" ");
</script>
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-label ${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()} title={tooltip} alt="" on:click={action} />
<style lang="scss" global>
.image-label {
.image-button {
width: auto;
height: auto;
border-radius: 2px;
+ .image-label.image-label {
+ .image-button.image-button {
margin-left: 8px;
}
}
@@ -1,12 +1,11 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { FillChoice } from "@graphite/messages";
import type { FillChoice, MenuDirection } from "@graphite/messages";
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
@@ -15,6 +14,7 @@
export let value: FillChoice;
export let disabled = false;
export let allowNone = false;
export let menuDirection: MenuDirection = "Bottom";
// export let allowTransparency = false; // TODO: Implement
export let tooltip: string | undefined = undefined;
@@ -25,16 +25,18 @@
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
</script>
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}>
<button {disabled} 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}
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
<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>
{/if}
{/if} -->
</button>
<ColorPicker
{open}
on:open={({ detail }) => (open = detail)}
{disabled}
colorOrGradient={value}
direction={menuDirection || "Bottom"}
on:open={({ detail }) => (open = detail)}
on:colorOrGradient={({ detail }) => {
value = detail;
dispatch("value", detail);
@@ -137,5 +139,9 @@
left: 50%;
bottom: 0;
}
&.direction-top > .floating-menu {
bottom: 100%;
}
}
</style>
@@ -13,6 +13,7 @@
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
export let gradient: Gradient;
export let disabled = false;
export let activeMarkerIndex = 0 as number | undefined;
// export let disabled = false;
// export let tooltip: string | undefined = undefined;
@@ -22,6 +23,8 @@
let deletionRestore: boolean | undefined = undefined;
function markerPointerDown(e: PointerEvent, index: number) {
if (disabled) return;
// Left-click to select and begin potentially dragging
if (e.button === BUTTON_LEFT) {
activeMarkerIndex = index;
@@ -47,6 +50,8 @@
}
function insertStop(e: MouseEvent) {
if (disabled) return;
if (e.button !== BUTTON_LEFT) return;
let position = markerPosition(e);
@@ -79,6 +84,8 @@
}
function deleteStop(e: KeyboardEvent) {
if (disabled) return;
if (e.key !== "Delete" && e.key !== "Backspace") return;
if (activeMarkerIndex === undefined) return;
@@ -88,6 +95,8 @@
}
function deleteStopByIndex(index: number) {
if (disabled) return;
if (gradient.stops.length <= 2) return;
gradient.stops.splice(index, 1);
@@ -103,6 +112,8 @@
}
function moveMarker(e: PointerEvent, index: number) {
if (disabled) return;
// Just in case the mouseup event is lost
if (e.buttons === 0) stopDrag();
@@ -120,6 +131,8 @@
}
export function setPosition(index: number, position: number) {
if (disabled) return;
const active = gradient.stops[index];
active.position = position;
gradient.stops.sort((a, b) => a.position - b.position);
@@ -131,6 +144,8 @@
}
function abortDrag() {
if (disabled) return;
if (activeMarkerIndex === undefined) return;
if (deletionRestore) {
@@ -143,6 +158,8 @@
}
function stopDrag() {
if (disabled) return;
removeEvents();
positionRestore = undefined;
@@ -152,19 +169,27 @@
}
function onPointerMove(e: PointerEvent) {
if (disabled) return;
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
}
function onPointerUp() {
if (disabled) return;
stopDrag();
}
function onMouseDown(e: MouseEvent) {
if (disabled) return;
const BUTTONS_RIGHT = 0b0000_0010;
if (e.buttons & BUTTONS_RIGHT) abortDrag();
}
function onKeyDown(e: KeyboardEvent) {
if (disabled) return;
if (e.key === "Escape") {
const element = markerTrack?.div();
if (element) preventEscapeClosingParentFloatingMenu(element);
@@ -193,25 +218,28 @@
document.removeEventListener("keydown", deleteStop);
});
// Future design notes:
//
// # Backend -> Frontend
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged.
// UpdateGradient(gradient)
// UpdateMarkers({ index, position, color }[])
//
// # Frontend -> Backend
// SendNewActive(index)
// SendPositions({ index, position }[])
// AddMarker(position)
// RemoveMarkers(index[])
// ResetMarkerToDefault(index)
// // We need a way to encode constraints on some markers, like locking them in place or preventing reordering
// // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers
// // We need the ability to multi-select markers and move them all at once
//
// We need a way to encode constraints on some markers, like locking them in place or preventing reordering
// We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers
// We need the ability to multi-select markers and move them all at once
</script>
<LayoutCol
class="spectrum-input"
classes={{ disabled }}
styles={{
"--gradient-start": gradient.firstColor()?.toHexOptionalAlpha() || "black",
"--gradient-end": gradient.lastColor()?.toHexOptionalAlpha() || "black",
@@ -232,6 +260,9 @@
viewBox="0 0 12 12"
>
<path class="inner-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
{#if disabled}
<path class="disabled-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
{/if}
<path
class="outer-border"
d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z"
@@ -269,6 +300,14 @@
border-radius: 2px;
}
&.disabled .gradient-strip {
transition: opacity 0.1s;
&:hover {
opacity: 0.5;
}
}
.marker-track {
margin-top: calc(24px - 16px - 12px);
margin-left: var(--marker-half-width);
@@ -294,28 +333,40 @@
.outer-border {
fill: var(--color-5-dullgray);
}
}
}
&:not(.active) {
.inner-fill:hover + .outer-border,
.outer-border:hover {
fill: var(--color-6-lowergray);
}
&.disabled .marker-track .marker {
.disabled-fill {
opacity: 0.5;
}
.outer-border {
fill: var(--color-4-dimgray);
}
}
&:not(.disabled) .marker-track .marker {
&:not(.active) {
.inner-fill:hover + .outer-border,
.outer-border:hover {
fill: var(--color-6-lowergray);
}
}
&.active {
.inner-fill {
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
}
&.active {
.inner-fill {
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
}
// Outer border when active
.outer-border {
fill: var(--color-e-nearwhite);
}
// Outer border when active
.outer-border {
fill: var(--color-e-nearwhite);
}
.inner-fill:hover + .outer-border,
.outer-border:hover {
fill: var(--color-f-white);
}
.inner-fill:hover + .outer-border,
.outer-border:hover {
fill: var(--color-f-white);
}
}
}
@@ -16,6 +16,7 @@
// Styling
export let centered = false;
export let minWidth = 0;
export let maxWidth = 0;
let className = "";
export { className as class };
@@ -63,7 +64,10 @@
<FieldInput
class={`text-input ${className}`.trim()}
classes={{ centered, ...classes }}
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
styles={{
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
}}
{value}
on:value
on:textFocused={onTextFocused}
@@ -0,0 +1,32 @@
<script lang="ts">
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
export let url: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: 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="" />
<style lang="scss" global>
.image-label {
width: auto;
height: auto;
border-radius: 2px;
background-image: var(--color-transparent-checkered-background);
background-size: var(--color-transparent-checkered-background-size);
background-position: var(--color-transparent-checkered-background-position);
background-repeat: var(--color-transparent-checkered-background-repeat);
+ .image-label.image-label {
margin-left: 8px;
}
}
</style>
@@ -1,14 +1,14 @@
<script lang="ts" context="module">
import Data from "@graphite/components/panels/Data.svelte";
import Document from "@graphite/components/panels/Document.svelte";
import Layers from "@graphite/components/panels/Layers.svelte";
import Properties from "@graphite/components/panels/Properties.svelte";
import Spreadsheet from "@graphite/components/panels/Spreadsheet.svelte";
const PANEL_COMPONENTS = {
Document,
Layers,
Properties,
Spreadsheet,
Data,
};
type PanelType = keyof typeof PANEL_COMPONENTS;
</script>
@@ -16,7 +16,7 @@
/**/ root: 100,
/* ├─ */ content: 80,
/* │ ├─ */ document: 70,
/* │ └─ */ spreadsheet: 30,
/* │ └─ */ data: 30,
/* └─ */ details: 20,
/* ├─ */ properties: 45,
/* └─ */ layers: 55,
@@ -148,23 +148,31 @@
bind:this={documentPanel}
/>
</LayoutRow>
{#if $portfolio.spreadsheetOpen}
{#if $portfolio.dataPanelOpen}
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["spreadsheet"] }} data-subdivision-name="spreadsheet">
<Panel panelType="Spreadsheet" tabLabels={[{ name: "Spreadsheet" }]} tabActiveIndex={0} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["data"] }} data-subdivision-name="data">
<Panel panelType="Data" tabLabels={[{ name: "Data" }]} tabActiveIndex={0} />
</LayoutRow>
{/if}
</LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
</LayoutRow>
</LayoutCol>
{#if $portfolio.propertiesPanelOpen || $portfolio.layersPanelOpen}
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
{#if $portfolio.propertiesPanelOpen}
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
</LayoutRow>
{/if}
{#if $portfolio.propertiesPanelOpen && $portfolio.layersPanelOpen}
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
{/if}
{#if $portfolio.layersPanelOpen}
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
</LayoutRow>
{/if}
</LayoutCol>
{/if}
</LayoutRow>
{#if $dialog.visible}
<Dialog />
+52 -15
View File
@@ -763,10 +763,16 @@ export class UpdateGraphFadeArtwork extends JsMessage {
readonly percentage!: number;
}
export class UpdateSpreadsheetState extends JsMessage {
export class UpdateDataPanelState extends JsMessage {
readonly open!: boolean;
}
readonly node!: bigint | undefined;
export class UpdatePropertiesPanelState extends JsMessage {
readonly open!: boolean;
}
export class UpdateLayersPanelState extends JsMessage {
readonly open!: boolean;
}
export class UpdateMouseCursor extends JsMessage {
@@ -981,9 +987,11 @@ export class ColorInput extends WidgetProps {
})
value!: FillChoice;
allowNone!: boolean;
disabled!: boolean;
allowNone!: boolean;
menuDirection!: MenuDirection | undefined;
// allowTransparency!: boolean; // TODO: Implement
@@ -1140,6 +1148,19 @@ export class ImageButton extends WidgetProps {
tooltip!: string | undefined;
}
export class ImageLabel extends WidgetProps {
url!: string;
@Transform(({ value }: { value: string }) => value || undefined)
width!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
height!: string | undefined;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
export type NumberInputMode = "Increment" | "Range";
@@ -1332,6 +1353,8 @@ export class TextInput extends WidgetProps {
minWidth!: number;
maxWidth!: number;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@@ -1383,6 +1406,7 @@ const widgetSubTypes = [
{ value: FontInput, name: "FontInput" },
{ value: IconButton, name: "IconButton" },
{ value: ImageButton, name: "ImageButton" },
{ value: ImageLabel, name: "ImageLabel" },
{ value: IconLabel, name: "IconLabel" },
{ value: NodeCatalog, name: "NodeCatalog" },
{ value: NumberInput, name: "NumberInput" },
@@ -1472,11 +1496,11 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
updates.diff.forEach((update) => {
// Find the object where the diff applies to
const diffObject = update.widgetPath.reduce((targetLayout, index) => {
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
if ("layout" in targetLayout) return targetLayout.layout[index];
const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => {
if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index];
if (targetLayout instanceof Widget) {
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) {
return targetLayout.props.popoverLayout[index];
@@ -1487,10 +1511,21 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
}
// This is a path traversal so we can assume from the backend that it exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if ("action" in targetLayout) return targetLayout.children![index];
return targetLayout[index];
if (targetLayout && "action" in targetLayout) return targetLayout.children![index];
return targetLayout?.[index];
}, layout.layout as UIItem);
// Exit if we failed to produce a valid patch for the existing layout.
// This means that the backend assumed an existing layout that doesn't exist in the frontend. This can happen, for
// example, if a panel is destroyed in the frontend but was never cleared in the backend, so the next time the backend
// tries to update the layout, it attempts to insert only the changes against the old layout that no longer exists.
if (diffObject === undefined) {
// eslint-disable-next-line no-console
console.error("In `patchWidgetLayout`, the `diffObject` is undefined. The layout has not been updated. See the source code comment above this error for hints.");
return;
}
// If this is a list with a length, then set the length to 0 to clear the list
if ("length" in diffObject) {
diffObject.length = 0;
@@ -1613,9 +1648,9 @@ export class UpdateMenuBarLayout extends JsMessage {
export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {}
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
export class UpdatePropertiesPanelLayout extends WidgetDiffUpdate {}
export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {}
export class UpdateDataPanelLayout extends WidgetDiffUpdate {}
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
@@ -1712,9 +1747,11 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateNodeThumbnail,
UpdateOpenDocumentsList,
UpdatePlatform,
UpdatePropertyPanelSectionsLayout,
UpdateSpreadsheetLayout,
UpdateSpreadsheetState,
UpdatePropertiesPanelLayout,
UpdateDataPanelLayout,
UpdateDataPanelState,
UpdatePropertiesPanelState,
UpdateLayersPanelState,
UpdateToolOptionsLayout,
UpdateToolShelfLayout,
UpdateViewportHolePunch,
+16 -12
View File
@@ -13,10 +13,9 @@ import {
TriggerOpenDocument,
UpdateActiveDocument,
UpdateOpenDocumentsList,
UpdateSpreadsheetState,
defaultWidgetLayout,
patchWidgetLayout,
UpdateSpreadsheetLayout,
UpdateDataPanelState,
UpdatePropertiesPanelState,
UpdateLayersPanelState,
} from "@graphite/messages";
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
@@ -27,9 +26,9 @@ export function createPortfolioState(editor: Editor) {
unsaved: false,
documents: [] as FrontendDocumentDetails[],
activeDocumentIndex: 0,
spreadsheetOpen: false,
spreadsheetNode: BigInt(0) as bigint | undefined,
spreadsheetWidgets: defaultWidgetLayout(),
dataPanelOpen: false,
propertiesPanelOpen: true,
layersPanelOpen: true,
});
// Set up message subscriptions on creation
@@ -107,16 +106,21 @@ export function createPortfolioState(editor: Editor) {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
}
});
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
editor.subscriptions.subscribeJsMessage(UpdateDataPanelState, async (updateDataPanelState) => {
update((state) => {
state.spreadsheetOpen = updateSpreadsheetState.open;
state.spreadsheetNode = updateSpreadsheetState.node;
state.dataPanelOpen = updateDataPanelState.open;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => {
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (updatePropertiesPanelState) => {
update((state) => {
patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout);
state.propertiesPanelOpen = updatePropertiesPanelState.open;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelState, async (updateLayersPanelState) => {
update((state) => {
state.layersPanelOpen = updateLayersPanelState.open;
return state;
});
});
+5
View File
@@ -17,6 +17,10 @@ export function createSubscriptionRouter() {
subscriptions[messageType.name] = callback;
};
const unsubscribeJsMessage = <T extends JsMessage>(messageType: new () => T) => {
delete subscriptions[messageType.name];
};
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => {
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
const messageMaker = messageMakers[messageType];
@@ -68,6 +72,7 @@ export function createSubscriptionRouter() {
return {
subscribeJsMessage,
unsubscribeJsMessage,
handleJsMessage,
};
}