Graphite/frontend/src/components/Editor.svelte

405 lines
14 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy, setContext } from "svelte";
import MainWindow from "/src/components/window/MainWindow.svelte";
import { createClipboardManager, destroyClipboardManager } from "/src/managers/clipboard";
import { createFontsManager, destroyFontsManager } from "/src/managers/fonts";
import { createHyperlinkManager, destroyHyperlinkManager } from "/src/managers/hyperlink";
import { createInputManager, destroyInputManager } from "/src/managers/input";
import { createLocalizationManager, destroyLocalizationManager } from "/src/managers/localization";
import { createPanicManager, destroyPanicManager } from "/src/managers/panic";
import { createPersistenceManager, destroyPersistenceManager } from "/src/managers/persistence";
import { createAppWindowStore, destroyAppWindowStore } from "/src/stores/app-window";
import { createColorPickerStore, destroyColorPickerStore } from "/src/stores/color-picker";
import { createDialogStore, destroyDialogStore } from "/src/stores/dialog";
import { createDocumentStore, destroyDocumentStore } from "/src/stores/document";
import { createFullscreenStore, destroyFullscreenStore } from "/src/stores/fullscreen";
import { createNodeGraphStore, destroyNodeGraphStore } from "/src/stores/node-graph";
import { createPortfolioStore, destroyPortfolioStore } from "/src/stores/portfolio";
import { createTooltipStore, destroyTooltipStore } from "/src/stores/tooltip";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
// Graphite Wasm editor and subscriptions router
export let subscriptions: SubscriptionsRouter;
export let editor: EditorWrapper;
setContext("subscriptions", subscriptions);
setContext("editor", editor);
const stores = {
dialog: createDialogStore(subscriptions, editor),
tooltip: createTooltipStore(subscriptions),
document: createDocumentStore(subscriptions),
fullscreen: createFullscreenStore(subscriptions),
nodeGraph: createNodeGraphStore(subscriptions),
portfolio: createPortfolioStore(subscriptions, editor),
appWindow: createAppWindowStore(subscriptions),
colorPicker: createColorPickerStore(subscriptions),
};
Object.entries(stores).forEach(([key, store]) => setContext(key, store));
onMount(() => {
createClipboardManager(subscriptions, editor);
createHyperlinkManager(subscriptions);
createLocalizationManager(subscriptions, editor);
createPanicManager(subscriptions);
createPersistenceManager(subscriptions, editor, stores.portfolio);
createFontsManager(subscriptions, editor);
createInputManager(subscriptions, editor, stores.dialog, stores.portfolio, stores.document);
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready.
// The backend handles idempotency, so this is safe to call again during HMR re-mounts.
editor.initAfterFrontendReady();
// Re-send all UI layouts from Rust so the frontend has them after an HMR re-mount
editor.resendAllLayouts();
});
onDestroy(() => {
// Stores
destroyDialogStore();
destroyTooltipStore();
destroyDocumentStore();
destroyFullscreenStore();
destroyNodeGraphStore();
destroyPortfolioStore();
destroyAppWindowStore();
destroyColorPickerStore();
// Managers
destroyClipboardManager();
destroyHyperlinkManager();
destroyLocalizationManager();
destroyPanicManager();
destroyPersistenceManager();
destroyFontsManager();
destroyInputManager();
});
</script>
<MainWindow />
<style lang="scss">
// Disable the spinning loading indicator
body::before,
body::after {
content: none !important;
}
:root {
// Replace usage of `-rgb` variants with CSS color() function to calculate alpha when browsers support it
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color() and https://caniuse.com/css-color-function
// Specifically, support for the relative syntax is needed: `color(from var(--color-0-black) srgb r g b / 0.5)` to convert black to 50% alpha
--color-0-black: #000;
--color-0-black-rgb: 0, 0, 0;
--color-1-nearblack: #111;
--color-1-nearblack-rgb: 17, 17, 17;
--color-2-mildblack: #222;
--color-2-mildblack-rgb: 34, 34, 34;
--color-3-darkgray: #333;
--color-3-darkgray-rgb: 51, 51, 51;
--color-4-dimgray: #444;
--color-4-dimgray-rgb: 68, 68, 68;
--color-5-dullgray: #555;
--color-5-dullgray-rgb: 85, 85, 85;
--color-6-lowergray: #666;
--color-6-lowergray-rgb: 102, 102, 102;
--color-7-middlegray: #777;
--color-7-middlegray-rgb: 109, 109, 109;
--color-8-uppergray: #888;
--color-8-uppergray-rgb: 136, 136, 136;
--color-9-palegray: #999;
--color-9-palegray-rgb: 153, 153, 153;
--color-a-softgray: #aaa;
--color-a-softgray-rgb: 170, 170, 170;
--color-b-lightgray: #bbb;
--color-b-lightgray-rgb: 187, 187, 187;
--color-c-brightgray: #ccc;
--color-c-brightgray-rgb: 204, 204, 204;
--color-d-mildwhite: #ddd;
--color-d-mildwhite-rgb: 221, 221, 221;
--color-e-nearwhite: #eee;
--color-e-nearwhite-rgb: 238, 238, 238;
--color-f-white: #fff;
--color-f-white-rgb: 255, 255, 255;
--color-error-red: #d6536e;
--color-error-red-rgb: 214, 83, 110;
--color-warning-yellow: #d5aa43;
--color-warning-yellow-rgb: 213, 170, 67;
--color-data-general: #cfcfcf;
--color-data-general-dim: #8a8a8a;
--color-data-number: #c9a699;
--color-data-number-dim: #886b60;
--color-data-artboard: #fbf9eb;
--color-data-artboard-dim: #b9b9a9;
--color-data-graphic: #68c587;
--color-data-graphic-dim: #37754c;
--color-data-raster: #e4bb72;
--color-data-raster-dim: #9a7b43;
--color-data-vector: #65bbe5;
--color-data-vector-dim: #417892;
--color-data-color: #ce6ea7;
--color-data-color-dim: #924071;
--color-data-gradient: #af81eb;
--color-data-gradient-dim: #6c489b;
--color-data-typography: #eea7a7;
--color-data-typography-dim: #955252;
--color-data-invalid: #d6536e; // Same as --color-error-red
--color-data-invalid-dim: #a7324a;
--color-overlay-blue: #00a8ff;
--color-none: white;
--color-none-repeat: no-repeat;
--color-none-position: center center;
// 24px tall, 48px wide
--color-none-size-24px: 60px 24px;
// Red diagonal slash (24px tall)
--color-none-image-24px: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 24"><line stroke="red" stroke-width="4px" x1="0" y1="27" x2="60" y2="-3" /></svg>\
');
// 32px tall, 64px wide
--color-none-size-32px: 80px 32px;
// Red diagonal slash (32px tall)
--color-none-image-32px: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 32"><line stroke="red" stroke-width="4px" x1="0" y1="36" x2="80" y2="-4" /></svg>\
');
--color-transparent-checkered-background:
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%),
linear-gradient(#ffffff, #ffffff);
--color-transparent-checkered-background-size: 16px 16px, 16px 16px, 16px 16px;
--color-transparent-checkered-background-position: 0 0, 8px 8px, 8px 8px;
--color-transparent-checkered-background-position-plus-one: 1px 1px, 9px 9px, 9px 9px;
--color-transparent-checkered-background-size-mini: 8px 8px, 8px 8px, 8px 8px;
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px, 4px 4px;
--color-transparent-checkered-background-repeat: repeat, repeat, repeat;
--inheritance-stripes-background: repeating-linear-gradient(
-45deg,
transparent 0px,
transparent calc((3px * sqrt(2) / 2) - 0.5px),
var(--color-5-dullgray) calc((3px * sqrt(2) / 2) - 0.5px),
var(--color-5-dullgray) calc((3px * sqrt(2) / 2) + 0.5px),
transparent calc((3px * sqrt(2) / 2) + 0.5px),
transparent calc(6px * sqrt(2) / 2)
);
--inheritance-dots-background-4-dimgray: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4" width="4px" height="4px" fill="%23444"><rect width="1" height="1" /><rect x="2" y="2" width="1" height="1" /></svg>\
');
--inheritance-dots-background-6-lowergray: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4" width="4px" height="4px" fill="%23666"><rect width="1" height="1" /><rect x="2" y="2" width="1" height="1" /></svg>\
');
// Array of 2x3 dots (fill: --color-e-nearwhite)
--icon-drag-grip: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 24" fill="%23eee">\
<circle cx="0.5" cy="1.5" r="0.5" /><circle cx="3.5" cy="1.5" r="0.5" />\
<circle cx="0.5" cy="4.5" r="0.5" /><circle cx="3.5" cy="4.5" r="0.5" />\
<circle cx="0.5" cy="7.5" r="0.5" /><circle cx="3.5" cy="7.5" r="0.5" />\
</svg>\
');
// Array of 2x3 dots (fill: --color-f-white)
--icon-drag-grip-hover: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 24" fill="%23fff">\
<circle cx="0.5" cy="1.5" r="0.5" /><circle cx="3.5" cy="1.5" r="0.5" />\
<circle cx="0.5" cy="4.5" r="0.5" /><circle cx="3.5" cy="4.5" r="0.5" />\
<circle cx="0.5" cy="7.5" r="0.5" /><circle cx="3.5" cy="7.5" r="0.5" />\
</svg>\
');
// Array of 2x3 dots (fill: --color-8-uppergray)
--icon-drag-grip-disabled: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 24" fill="%23888">\
<circle cx="0.5" cy="1.5" r="0.5" /><circle cx="3.5" cy="1.5" r="0.5" />\
<circle cx="0.5" cy="4.5" r="0.5" /><circle cx="3.5" cy="4.5" r="0.5" />\
<circle cx="0.5" cy="7.5" r="0.5" /><circle cx="3.5" cy="7.5" r="0.5" />\
</svg>\
');
// Arrow triangle (fill: --color-e-nearwhite)
--icon-expand-collapse-arrow: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23eee" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\
');
// Arrow triangle (fill: --color-f-white)
--icon-expand-collapse-arrow-hover: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23fff" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\
');
// Arrow triangle (fill: --color-8-uppergray)
--icon-expand-collapse-arrow-disabled: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23888" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>\
');
}
html,
body {
margin: 0;
height: 100%;
background: var(--color-2-mildblack);
overscroll-behavior: none;
-webkit-user-select: none; // Still required by Safari as of 2025
user-select: none;
}
body.cursor-hidden * {
cursor: none !important;
}
// Needed for the viewport hole punch on desktop
html:has(body > .viewport-hole-punch),
body:has(> .viewport-hole-punch) {
background: none;
}
// The default value of `auto` from the CSS spec is a footgun with flexbox layouts:
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
* {
min-width: 0;
min-height: 0;
}
html,
body,
input,
textarea,
button {
font-family: "Source Sans Pro", Arial, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 1;
tab-size: 4;
color: var(--color-e-nearwhite);
}
body,
textarea,
input {
&::selection {
background-color: var(--color-4-dimgray);
// Target only Safari
@supports (background: -webkit-named-image(i)) {
& {
// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering
// https://stackoverflow.com/a/71753552/775283
background-color: rgba(var(--color-4-dimgray-rgb), calc(254 / 255));
}
}
}
}
svg,
img {
display: block;
}
.layout-row,
.layout-col {
.scrollable-x,
.scrollable-y {
overflow: hidden;
scrollbar-width: thin;
scrollbar-color: var(--color-4-dimgray) transparent;
}
.scrollable-x.scrollable-y {
overflow: auto;
}
.scrollable-x:not(.scrollable-y) {
scrollbar-width: none;
overflow: auto hidden;
}
.scrollable-y:not(.scrollable-x) {
overflow: hidden auto;
}
}
// List of all elements that should show an outline when focused by tabbing or by clicking the element
.dropdown-input .dropdown-box,
.font-input .dropdown-box {
&:focus {
outline: 1px dashed var(--color-e-nearwhite);
outline-offset: -1px;
}
}
// List of all elements that should show an outline when focused by tabbing, but not by clicking the element
.icon-button,
.text-button,
.popover-button,
.color-input > button,
.color-picker .preset-color,
.working-colors-input .swatch > button,
.radio-input button,
.menu-list,
.menu-list-button .entry,
.layer-tree .expand-arrow,
.widget-section .header {
&:focus-visible {
outline: 1px dashed var(--color-e-nearwhite);
outline-offset: -1px;
}
// Variant: dark outline over light colors
&.preset-color.white,
&.text-button.emphasized {
&:focus-visible {
outline: 1px dashed var(--color-2-mildblack);
}
}
}
// Checkbox needs to apply the focus outline to its sibling label
.optional-input input:focus-visible + label,
.checkbox-input input:focus-visible + label {
outline: 1px dashed var(--color-e-nearwhite);
outline-offset: -1px;
}
// Variant: dark outline over light colors (when the checkbox is checked)
:not(.optional-input) > .checkbox-input input:focus-visible + label.checked {
outline: 1px dashed var(--color-2-mildblack);
}
@font-face {
font-family: "Source Sans Pro";
font-weight: 400;
font-style: normal;
font-stretch: normal;
src: url("/node_modules/source-sans-pro/WOFF2/TTF/SourceSansPro-Regular.ttf.woff2") format("woff2");
}
@font-face {
font-family: "Source Sans Pro";
font-weight: 400;
font-style: italic;
font-stretch: normal;
src: url("/node_modules/source-sans-pro/WOFF2/TTF/SourceSansPro-It.ttf.woff2") format("woff2");
}
@font-face {
font-family: "Source Sans Pro";
font-weight: 700;
font-style: normal;
font-stretch: normal;
src: url("/node_modules/source-sans-pro/WOFF2/TTF/SourceSansPro-Bold.ttf.woff2") format("woff2");
}
@font-face {
font-family: "Source Sans Pro";
font-weight: 700;
font-style: italic;
font-stretch: normal;
src: url("/node_modules/source-sans-pro/WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
font-weight: 400;
font-style: normal;
font-stretch: normal;
src: url("/node_modules/source-code-pro/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2") format("woff2");
}
</style>