Dialog redesign and content revamp (#1409)

* Revamp the content and design of dialogs

* Add the Licenses dialog
This commit is contained in:
Keavon Chambers
2023-09-01 01:58:20 -07:00
committed by GitHub
parent 5acb2cff06
commit a112ab27cf
52 changed files with 796 additions and 451 deletions
+3 -5
View File
@@ -1,7 +1,5 @@
Copyright (c) 2021-2023 Keavon Chambers
Copyright (c) 2021-2023 Graphite Labs, LLC.
The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and copyrights are held by the author for the creative works contained as files herein.
The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and full copyright is held by the rightsholder for the creative works contained as files herein.
Parties interested in using Graphite source code in a capacity that deploys the Graphite editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder. The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology.
The author and rightsholder, Keavon Chambers, may be reached through the email address listed at https://graphite.rs/contact/ or https://keavon.com.
Parties interested in using Graphite source code in a capacity that deploys the Graphite editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder (see https://graphite.rs/contact/ for contact info). The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology.
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="m6,1c2.76,0,5,2.24,5,5s-2.24,5-5,5S1,8.76,1,6,3.24,1,6,1m0-1C2.68,0,0,2.69,0,6s2.68,6,6,6,6-2.68,6-6S9.31,0,6,0h0Zm2.47,8.47c.2-.2.2-.51,0-.71l-1.97-1.97v-2.79c0-.28-.22-.5-.5-.5s-.5.22-.5.5v3c0,.13.05.26.15.35l2.12,2.12c.1.1.23.15.35.15s.26-.05.35-.15z" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="m6,1c2.76,0,5,2.24,5,5s-2.24,5-5,5S1,8.76,1,6c0-2.76,2.24-5,5-5m0-1C2.69,0,0,2.69,0,6s2.69,6,6,6,6-2.69,6-6S9.31,0,6,0m2.83,3.88l-.71-.71-2.12,2.12-2.12-2.12-.71.71,2.12,2.12-2.12,2.12.71.71,2.12-2.12,2.12,2.12.71-.71-2.12-2.12,2.12-2.12z" />
</svg>

After

Width:  |  Height:  |  Size: 321 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="m12,1.63C11.94.7,11.17,0,10.25,0h-6.25C2.9,0,2,.9,2,2v6.5H0v1.75c0,.92.7,1.69,1.62,1.75h6.12c.95-.05,1.7-.8,1.75-1.75v-6.25h2.5V1.63ZM1.63,11c-.37-.06-.63-.38-.62-.75v-.75h5v.75c0,.26.07.52.19.75H1.63Zm6.12,0c-.39-.05-.7-.36-.75-.75v-1.75h-4V2c0-.55.45-1,1-1h4.71c-.14.31-.21.66-.21,1v8.25c-.05.39-.36.7-.75.75m3.25-8h-1.5v-1c0-.3.07-1,.75-1,.37,0,.69.26.75.62v1.37Zm-3.75-.5h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25m0,2h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25m0,2h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25z" />
</svg>

After

Width:  |  Height:  |  Size: 698 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m2,1h6v2H2V1Zm0,12h7v2H2v-2ZM1,5h3v2H1v-2Zm-1,4h5v2H0v-2Zm7,0h4v2h-4v-2ZM10,1h4v2h-4V1Zm1,12h3v2h-3v-2ZM6,5h9v2H6v-2Zm7,4h3v2h-3v-2z" />
</svg>

After

Width:  |  Height:  |  Size: 215 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m6,7H2c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1ZM2,2v4h4V2H2Zm4,13H2c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4v-4H2Zm12-3h-4c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4V2h-4Zm4,13h-4c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4v-4h-4z" />
</svg>

After

Width:  |  Height:  |  Size: 457 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m16,2c0-1.1-.9-2-2-2H5.5c-1.38,0-2.5,1.12-2.5,2.5v9.5H0v2c0,1.1.9,2,2,2h9c1.1,0,2-.9,2-2V5h3v-3ZM2,15c-.55,0-1-.45-1-1v-1h8v1c0,.35.09.7.27,1H2Zm9,0c-.55,0-1-.45-1-1v-2h-6V2.5c0-.83.67-1.5,1.5-1.5h6.82c-.22.47-.33.98-.32,1.5v11.5c0,.55-.45,1-1,1m4-11h-2v-1.5c0-1.5.75-1.5,1-1.5.55,0,1,.45,1,1v2Zm-4.5-1h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5m0,3h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5m0,3h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5z" />
</svg>

After

Width:  |  Height:  |  Size: 589 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m4.56,15.92c1.01.06,1.28.08,2.53.08,2.95,0,3.89.02,6.12-.08.23-1.32.23-2.68,0-4-.44-2.54.53-2.46,1.4-4.45,1.54-3.49,1.39-4.97,1.39-4.97.07-.76-.48-1.43-1.24-1.51-.75.08-1.3.75-1.24,1.51-.14,1.73-1.05,3.29-2.49,4.26-2.36,1.66-5.5.45-8.39,2.83C.67,11.21.51,15.02.49,15.51c.82.19,1.65.29,2.49.32.08-1.09.35-2.16.8-3.16,0,0,.75.61.77,3.25ZM7.65,0c1.74,0,3.15,1.41,3.15,3.15s-1.41,3.15-3.15,3.15-3.15-1.41-3.15-3.15S5.91,0,7.65,0z" />
</svg>

After

Width:  |  Height:  |  Size: 508 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m8,0C3.59,0,0,3.59,0,8s3.59,8,8,8,8-3.59,8-8S12.41,0,8,0Zm6.1,5h-2.39c-.26-1.33-.69-2.47-1.24-3.33,1.58.62,2.88,1.82,3.63,3.33Zm.7,3c0,.7-.11,1.37-.31,2h-2.62c.08-.64.12-1.31.12-2s-.04-1.36-.12-2h2.62c.2.63.31,1.3.31,2Zm-13.6,0c0-.7.11-1.37.31-2h2.62c-.08.64-.12,1.31-.12,2s.04,1.36.12,2H1.51c-.2-.63-.31-1.3-.31-2Zm3.9,0c0-.71.06-1.37.14-2h5.52c.08.63.14,1.29.14,2s-.06,1.37-.14,2h-5.52c-.08-.63-.14-1.29-.14-2ZM7.49,1.23c.17-.01.34-.03.51-.03s.34.01.51.03c.84.41,1.65,1.79,2.08,3.77h-5.17c.43-1.98,1.24-3.37,2.08-3.77Zm-1.96.44c-.55.86-.98,2-1.24,3.33H1.9c.75-1.51,2.05-2.71,3.63-3.33ZM1.9,11h2.39c.26,1.33.69,2.47,1.24,3.33-1.58-.62-2.88-1.82-3.63-3.33Zm6.61,3.78c-.17.01-.34.03-.51.03s-.34-.01-.51-.03c-.84-.4-1.65-1.79-2.08-3.78h5.17c-.43,1.98-1.24,3.37-2.08,3.78Zm1.96-.44c.55-.86.98-2,1.24-3.33h2.39c-.75,1.51-2.05,2.71-3.63,3.33z" />
</svg>

After

Width:  |  Height:  |  Size: 920 B

+1 -1
View File
@@ -44,7 +44,7 @@
createPanicManager(editor, dialog);
createPersistenceManager(editor, portfolio);
let dragManagerDestructor = createDragManager();
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, fullscreen);
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
onMount(() => {
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
@@ -1,11 +1,14 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { githubUrl } from "@graphite/io-managers/panic";
import { wipeDocuments } from "@graphite/io-managers/persistence";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
import type { DialogState } from "@graphite/state-providers/dialog";
@@ -20,24 +23,52 @@
});
</script>
<!-- TODO: Use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog for improved accessibility -->
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={self} data-dialog-modal>
<LayoutRow>
<LayoutCol class="icon-column">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
</LayoutCol>
<LayoutCol class="main-column">
{#if $dialog.widgets.layout.length > 0}
<WidgetLayout layout={$dialog.widgets} class="details" />
<LayoutRow class="header-area">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
<TextLabel>{$dialog.title}</TextLabel>
</LayoutRow>
<LayoutRow class="content">
<LayoutCol class="column-1">
{#if $dialog.column1.layout.length > 0}
<WidgetLayout layout={$dialog.column1} class="details" />
{/if}
{#if ($dialog.crashDialogButtons?.length || NaN) > 0}
<LayoutRow class="panic-buttons-row">
{#each $dialog.crashDialogButtons || [] as button, index (index)}
<TextButton action={() => button.callback?.()} {...button.props} />
{/each}
</LayoutRow>
{#if $dialog.panicDetails}
<div class="widget-layout details">
<div class="widget-row"><TextLabel bold={true}>The editor crashed — sorry about that</TextLabel></div>
<div class="widget-row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
<div class="widget-row"><TextButton label="Report Bug" icon="Warning" noBackground={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
<div class="widget-row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
<div class="widget-row">
<TextButton
label="Clear Saved Documents"
icon="Trash"
noBackground={true}
action={async () => {
await wipeDocuments();
window.location.reload();
}}
/>
</div>
</div>
{/if}
</LayoutCol>
{#if $dialog.column2.layout.length > 0}
<LayoutCol class="column-2">
<WidgetLayout layout={$dialog.column2} class="details" />
</LayoutCol>
{/if}
</LayoutRow>
<LayoutRow class="footer-area">
{#if $dialog.buttons.layout.length > 0}
<WidgetLayout layout={$dialog.buttons} class="details" />
{/if}
{#if $dialog.panicDetails}
<TextButton label="Copy Error Log" action={() => navigator.clipboard.writeText($dialog.panicDetails)} />
<TextButton label="Reload" emphasized={true} action={() => window.location.reload()} />
{/if}
</LayoutRow>
</FloatingMenu>
@@ -50,32 +81,45 @@
> .floating-menu-container > .floating-menu-content {
pointer-events: auto;
padding: 24px;
padding: 0;
}
.icon-column {
margin-right: 24px;
.header-area,
.footer-area {
background: var(--color-1-nearblack);
}
.header-area,
.footer-area,
.content {
padding: 16px 24px;
}
.header-area {
border-radius: 4px 4px 0 0;
.icon-label {
width: 80px;
height: 80px;
width: 24px;
height: 24px;
}
&.file,
&.copy {
width: 60px;
svg {
width: 80px;
height: 80px;
margin: 0 -10px;
}
}
.text-label {
margin-left: 12px;
line-height: 24px;
}
}
.main-column {
.content {
margin: -4px 0;
.column-1 + .column-2 {
margin-left: 48px;
.text-button {
justify-content: left;
}
}
.details.text-label {
-webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
user-select: text;
@@ -84,13 +128,22 @@
height: auto;
}
.radio-input button {
flex-grow: 1;
}
// Used by the "Open Demo Artwork" dialog
.image-label {
border-radius: 2px;
}
}
.panic-buttons-row {
height: 32px;
align-items: center;
.footer-area {
border-radius: 0 0 4px 4px;
justify-content: right;
.text-button {
min-width: 96px;
}
}
}
@@ -4,7 +4,7 @@
</script>
<script lang="ts">
import { afterUpdate, createEventDispatcher, tick } from "svelte";
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -103,9 +103,42 @@
wasOpen = isOpen;
}
// Gets the client bounds of the elements and apply relevant styles to them
// TODO: Use DOM attribute bindings more whilst not causing recursive updates
onMount(() => {
// Measure the content and round up its width and height to the nearest even integer.
// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
const floatingMenuContentDiv = floatingMenuContent?.div();
if (type === "Dialog" && floatingMenuContentDiv) {
// TODO: Also use https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver to detect any changes which may affect the size of the content.
// TODO: The current method only notices when the dialog size increases but can't detect when it decreases.
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
let { width, height } = entry.contentRect;
width = Math.ceil(width);
if (width % 2 === 1) width += 1;
height = Math.ceil(height);
if (height % 2 === 1) height += 1;
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
floatingMenuContentDiv.style.setProperty("min-width", width === 0 ? "unset" : `${width}px`);
floatingMenuContentDiv.style.setProperty("min-height", height === 0 ? "unset" : `${height}px`);
});
});
resizeObserver.observe(floatingMenuContentDiv);
}
});
afterUpdate(() => {
// Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one
const floatingMenuContentDiv = floatingMenuContent?.div();
if (type === "Dialog" && floatingMenuContentDiv) {
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
floatingMenuContentDiv.style.setProperty("min-width", "unset");
floatingMenuContentDiv.style.setProperty("min-height", "unset");
}
// Gets the client bounds of the elements and apply relevant styles to them
// TODO: Use DOM attribute bindings more whilst not causing recursive updates
// Turning measuring on and off both causes the component to change, which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
});
@@ -69,13 +69,6 @@
editor.instance.updateLayout(layoutTarget, widgets[index].widgetId, value);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// function exclude<T extends Record<string, any>>(props: T, additional?: (keyof T)[]): Pick<T, Exclude<keyof T, "kind" | (typeof additional extends Array<infer K> ? K : never)>> {
// const exclusions = ["kind", ...(additional || [])];
// return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
// }
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> {
const exclusions = ["kind", ...(additional || [])];
@@ -7,6 +7,7 @@
export let label: string;
export let icon: IconName | undefined = undefined;
export let emphasized = false;
export let noBackground = false;
export let minWidth = 0;
export let disabled = false;
export let tooltip: string | undefined = undefined;
@@ -21,6 +22,7 @@
class="text-button"
class:emphasized
class:disabled
class:no-background={noBackground}
class:sharp-right-corners={sharpRightCorners}
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
title={tooltip}
@@ -76,10 +78,26 @@
}
}
&.text-button + .text-button {
&.no-background {
&:not(:hover) {
background: none;
}
.icon-label {
margin-right: 4px;
}
}
.widget-row > & + .text-button,
.layout-row > & + .text-button {
margin-left: 8px;
}
.widget-column > & + .text-button,
.layout-column > & + .text-button {
margin-top: 8px;
}
.icon-label {
position: relative;
left: -4px;
@@ -120,7 +120,7 @@
<table>
<tr>
<td>
<TextButton label="New Document" icon="File" action={() => editor.instance.newDocumentDialog()} />
<TextButton label="New Document" icon="File" noBackground={true} action={() => editor.instance.newDocumentDialog()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
@@ -128,7 +128,7 @@
</tr>
<tr>
<td>
<TextButton label="Open Document" icon="Folder" action={() => editor.instance.openDocument()} />
<TextButton label="Open Document" icon="Folder" noBackground={true} action={() => editor.instance.openDocument()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
@@ -136,7 +136,7 @@
</tr>
<tr>
<td colspan="2">
<TextButton label="Open Demo Artwork" icon="Image" action={() => editor.instance.demoArtworkDialog()} />
<TextButton label="Open Demo Artwork" icon="Image" noBackground={true} action={() => editor.instance.demoArtworkDialog()} />
</td>
</tr>
</table>
@@ -289,10 +289,6 @@
td {
padding: 0;
}
.text-button:not(:hover) {
background: none;
}
}
}
}
+6 -5
View File
@@ -1,6 +1,7 @@
import { get } from "svelte/store";
import { type DialogState } from "@graphite/state-providers/dialog";
import { type DocumentState } from "@graphite/state-providers/document";
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
import { type PortfolioState } from "@graphite/state-providers/portfolio";
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
@@ -16,7 +17,7 @@ type EventListenerTarget = {
removeEventListener: typeof window.removeEventListener;
};
export function createInputManager(editor: Editor, dialog: DialogState, document: PortfolioState, fullscreen: FullscreenState): () => void {
export function createInputManager(editor: Editor, dialog: DialogState, portfolio: PortfolioState, document: DocumentState, fullscreen: FullscreenState): () => void {
const app = window.document.querySelector("[data-app-container]") as HTMLElement | undefined;
app?.focus();
@@ -58,8 +59,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
// Keyboard events
async function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): Promise<boolean> {
// Don't redirect when a modal is covering the workspace
if (get(dialog).visible) return false;
// Don't redirect when a modal, or the overlaid graph, is covering the workspace
if (get(dialog).visible || get(document).graphViewOverlayOpen) return false;
const key = await getLocalizedScanCode(e);
@@ -239,7 +240,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
}
async function onBeforeUnload(e: BeforeUnloadEvent): Promise<void> {
const activeDocument = get(document).documents[get(document).activeDocumentIndex];
const activeDocument = get(portfolio).documents[get(portfolio).activeDocumentIndex];
if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id);
// Skip the message if the editor crashed, since work is already lost
@@ -248,7 +249,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
// Skip the message during development, since it's annoying when testing
if (await editor.instance.inDevelopmentMode()) return;
const allDocumentsSaved = get(document).documents.reduce((acc, doc) => acc && doc.isSaved, true);
const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.isSaved, true);
if (!allDocumentsSaved) {
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
e.preventDefault();
+4 -4
View File
@@ -2,10 +2,10 @@ import { type Editor } from "@graphite/wasm-communication/editor";
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/wasm-communication/messages";
export function createLocalizationManager(editor: Editor): void {
function localizeTimestamp(utc: string): string {
function localizeTimestamp(utc: string): { timestamp: string, year: string } {
// Timestamp
const date = new Date(utc);
if (Number.isNaN(date.getTime())) return utc;
if (Number.isNaN(date.getTime())) return { timestamp: utc, year: `${new Date().getFullYear()}` };
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
.formatToParts(new Date())
@@ -14,12 +14,12 @@ export function createLocalizationManager(editor: Editor): void {
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
const timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
const timezoneNameString = timezoneName?.value;
return `${dateString} ${timeString} ${timezoneNameString}`;
return { timestamp: `${dateString} ${timeString} ${timezoneNameString}`, year: `${date.getFullYear()}` };
}
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized);
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
});
}
+3 -40
View File
@@ -1,11 +1,8 @@
import { wipeDocuments } from "@graphite/io-managers/persistence";
import { type DialogState } from "@graphite/state-providers/dialog";
import { type IconName } from "@graphite/utility-functions/icons";
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
import { stripIndents } from "@graphite/utility-functions/strip-indents";
import { type Editor } from "@graphite/wasm-communication/editor";
import type { TextLabel } from "@graphite/wasm-communication/messages";
import { type TextButtonWidget, type WidgetLayout, Widget, DisplayDialogPanic } from "@graphite/wasm-communication/messages";
import { DisplayDialogPanic } from "@graphite/wasm-communication/messages";
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
// Code panic dialog and console error
@@ -19,45 +16,11 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
// eslint-disable-next-line no-console
console.error(panicDetails);
const crashDialog = prepareCrashDialog(displayDialogPanic.header, displayDialogPanic.description, panicDetails);
dialogState.createCrashDialog(...crashDialog);
dialogState.createCrashDialog(panicDetails);
});
}
function prepareCrashDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
const headerLabel: TextLabel = { kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" };
const detailsLabel: TextLabel = { kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" };
const widgets: WidgetLayout = {
layout: [{ rowWidgets: [new Widget(headerLabel, 0n)] }, { rowWidgets: [new Widget(detailsLabel, 1n)] }],
layoutTarget: undefined,
};
const reloadButton: TextButtonWidget = {
callback: async () => window.location.reload(),
props: { kind: "TextButton", label: "Reload", emphasized: true, minWidth: 96 },
};
const copyErrorLogButton: TextButtonWidget = {
callback: async () => navigator.clipboard.writeText(panicDetails),
props: { kind: "TextButton", label: "Copy Error Log", emphasized: false, minWidth: 96 },
};
const reportOnGithubButton: TextButtonWidget = {
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
props: { kind: "TextButton", label: "Report Bug", emphasized: false, minWidth: 96 },
};
const clearPersistedDataButton: TextButtonWidget = {
callback: async () => {
await wipeDocuments();
window.location.reload();
},
props: { kind: "TextButton", label: "Clear Saved Data", emphasized: false, minWidth: 96 },
};
const crashDialogButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton, clearPersistedDataButton];
return ["Warning", widgets, crashDialogButtons];
}
function githubUrl(panicDetails: string): string {
export function githubUrl(panicDetails: string): string {
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
let body = stripIndents`
+37 -13
View File
@@ -2,23 +2,25 @@ import {writable} from "svelte/store";
import { type IconName } from "@graphite/utility-functions/icons";
import { type Editor } from "@graphite/wasm-communication/editor";
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, patchWidgetLayout } from "@graphite/wasm-communication/messages";
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDialogState(editor: Editor) {
const { subscribe, update } = writable({
visible: false,
title: "",
icon: "" as IconName,
widgets: defaultWidgetLayout(),
buttons: defaultWidgetLayout(),
column1: defaultWidgetLayout(),
column2: defaultWidgetLayout(),
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor instance has panicked
crashDialogButtons: undefined as undefined | TextButtonWidget[],
panicDetails: "",
});
function dismissDialog(): void {
update((state) => {
// Disallow dismissing the crash dialog since it can confuse users why the app stopped responding if they dismiss it without realizing what it means
if (!state.crashDialogButtons) state.visible = false;
if (state.panicDetails === "") state.visible = false;
return state;
});
@@ -26,12 +28,18 @@ export function createDialogState(editor: Editor) {
// Creates a crash dialog from JS once the editor has panicked.
// Normal dialogs are created in the Rust backend, but for the crash dialog, the editor instance has panicked so it cannot respond to widget callbacks.
function createCrashDialog(icon: IconName, widgets: WidgetLayout, crashDialogButtons: TextButtonWidget[]): void {
function createCrashDialog(panicDetails: string): void {
update((state) => {
state.visible = true;
state.icon = icon;
state.widgets = widgets;
state.crashDialogButtons = crashDialogButtons;
state.icon = "Failure";
state.title = "Crash";
state.panicDetails = panicDetails;
state.column1 = defaultWidgetLayout();
state.column2 = defaultWidgetLayout();
state.buttons = defaultWidgetLayout();
return state;
});
}
@@ -40,15 +48,31 @@ export function createDialogState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(DisplayDialog, (displayDialog) => {
update((state) => {
state.visible = true;
state.title = displayDialog.title;
state.icon = displayDialog.icon;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
editor.subscriptions.subscribeJsMessage(UpdateDialogButtons, (updateDialogButtons) => {
update((state) => {
patchWidgetLayout(state.widgets, updateDialogDetails);
patchWidgetLayout(state.buttons, updateDialogButtons);
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn1, (updateDialogColumn1) => {
update((state) => {
patchWidgetLayout(state.column1, updateDialogColumn1);
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn2, (updateDialogColumn2) => {
update((state) => {
patchWidgetLayout(state.column2, updateDialogColumn2);
state.crashDialogButtons = undefined;
return state;
});
});
@@ -57,7 +81,7 @@ export function createDialogState(editor: Editor) {
return {
subscribe,
dismissDialog,
createCrashDialog: createCrashDialog,
createCrashDialog,
};
}
export type DialogState = ReturnType<typeof createDialogState>;
@@ -1,4 +1,3 @@
import {tick} from "svelte";
import {writable} from "svelte/store";
import { type Editor } from "@graphite/wasm-communication/editor";
+8 -3
View File
@@ -90,10 +90,15 @@ export function createPortfolioState(editor: Editor) {
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
// Rasterize the SVG to an image file
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
try {
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
// Have the browser download the file to the user's disk
downloadFileBlob(name, blob);
} catch {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
}
// Have the browser download the file to the user's disk
downloadFileBlob(name, blob);
});
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {
+18 -2
View File
@@ -11,9 +11,11 @@ const GRAPHICS = {
import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
import Failure from "@graphite-frontend/assets/icon-12px-solid/failure.svg";
import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.svg";
@@ -30,6 +32,7 @@ import KeyboardOption from "@graphite-frontend/assets/icon-12px-solid/keyboard-o
import KeyboardShift from "@graphite-frontend/assets/icon-12px-solid/keyboard-shift.svg";
import KeyboardSpace from "@graphite-frontend/assets/icon-12px-solid/keyboard-space.svg";
import KeyboardTab from "@graphite-frontend/assets/icon-12px-solid/keyboard-tab.svg";
import License12px from "@graphite-frontend/assets/icon-12px-solid/license-12px.svg";
import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
@@ -47,9 +50,11 @@ const SOLID_12PX = {
Add: { svg: Add, size: 12 },
Checkmark: { svg: Checkmark, size: 12 },
CloseX: { svg: CloseX, size: 12 },
Delay: { svg: Delay, size: 12 },
DropdownArrow: { svg: DropdownArrow, size: 12 },
Edit12px: { svg: Edit12px, size: 12 },
Empty12px: { svg: Empty12px, size: 12 },
Failure: { svg: Failure, size: 12 },
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
FullscreenExit: { svg: FullscreenExit, size: 12 },
Grid: { svg: Grid, size: 12 },
@@ -66,6 +71,7 @@ const SOLID_12PX = {
KeyboardShift: { svg: KeyboardShift, size: 12 },
KeyboardSpace: { svg: KeyboardSpace, size: 12 },
KeyboardTab: { svg: KeyboardTab, size: 12 },
License12px: { svg: License12px, size: 12 },
Link: { svg: Link, size: 12 },
Overlays: { svg: Overlays, size: 12 },
Remove: { svg: Remove, size: 12 },
@@ -95,6 +101,7 @@ import BooleanUnion from "@graphite-frontend/assets/icon-16px-solid/boolean-unio
import CheckboxChecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-checked.svg";
import CheckboxUnchecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-unchecked.svg";
import Copy from "@graphite-frontend/assets/icon-16px-solid/copy.svg";
import Credits from "@graphite-frontend/assets/icon-16px-solid/credits.svg";
import CustomColor from "@graphite-frontend/assets/icon-16px-solid/custom-color.svg";
import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg";
import Eyedropper from "@graphite-frontend/assets/icon-16px-solid/eyedropper.svg";
@@ -107,13 +114,15 @@ import Folder from "@graphite-frontend/assets/icon-16px-solid/folder.svg";
import GraphiteLogo from "@graphite-frontend/assets/icon-16px-solid/graphite-logo.svg";
import GraphViewClosed from "@graphite-frontend/assets/icon-16px-solid/graph-view-closed.svg";
import GraphViewOpen from "@graphite-frontend/assets/icon-16px-solid/graph-view-open.svg";
import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg";
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
import NodeArtboard from "@graphite-frontend/assets/icon-16px-solid/node-artboard.svg";
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
import NodeGradient from "@graphite-frontend/assets/icon-16px-solid/node-gradient.svg";
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
import NodeImaginate from "@graphite-frontend/assets/icon-16px-solid/node-imaginate.svg";
import NodeMagicWand from "@graphite-frontend/assets/icon-16px-solid/node-magic-wand.svg";
import NodeMask from "@graphite-frontend/assets/icon-16px-solid/node-mask.svg";
@@ -137,6 +146,8 @@ import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-
import ViewportDesignMode from "@graphite-frontend/assets/icon-16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@graphite-frontend/assets/icon-16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@graphite-frontend/assets/icon-16px-solid/viewport-select-mode.svg";
import Volunteer from "@graphite-frontend/assets/icon-16px-solid/volunteer.svg";
import Website from "@graphite-frontend/assets/icon-16px-solid/website.svg";
import WorkingColorsPrimary from "@graphite-frontend/assets/icon-16px-solid/working-colors-primary.svg";
import WorkingColorsSecondary from "@graphite-frontend/assets/icon-16px-solid/working-colors-secondary.svg";
import ZoomIn from "@graphite-frontend/assets/icon-16px-solid/zoom-in.svg";
@@ -158,6 +169,7 @@ const SOLID_16PX = {
CheckboxChecked: { svg: CheckboxChecked, size: 16 },
CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 },
Copy: { svg: Copy, size: 16 },
Credits: { svg: Credits, size: 16 },
CustomColor: { svg: CustomColor, size: 16 },
Edit: { svg: Edit, size: 16 },
Eyedropper: { svg: Eyedropper, size: 16 },
@@ -170,13 +182,15 @@ const SOLID_16PX = {
GraphiteLogo: { svg: GraphiteLogo, size: 16 },
GraphViewClosed: { svg: GraphViewClosed, size: 16 },
GraphViewOpen: { svg: GraphViewOpen, size: 16 },
IconsGrid: { svg: IconsGrid, size: 16 },
Image: { svg: Image, size: 16 },
Layer: { svg: Layer, size: 16 },
License: { svg: License, size: 16 },
NodeArtboard: { svg: NodeArtboard, size: 16 },
NodeBlur: { svg: NodeBlur, size: 16 },
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
NodeGradient: { svg: NodeGradient, size: 16 },
Image: { svg: Image, size: 16 },
NodeImaginate: { svg: NodeImaginate, size: 16 },
NodeMagicWand: { svg: NodeMagicWand, size: 16 },
NodeMask: { svg: NodeMask, size: 16 },
@@ -200,6 +214,8 @@ const SOLID_16PX = {
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
Volunteer: { svg: Volunteer, size: 16 },
Website: { svg: Website, size: 16 },
WorkingColorsPrimary: { svg: WorkingColorsPrimary, size: 16 },
WorkingColorsSecondary: { svg: WorkingColorsSecondary, size: 16 },
ZoomIn: { svg: ZoomIn, size: 16 },
@@ -40,6 +40,8 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
if (!width || !height) throw new Error("Width and height must be nonzero when given to rasterizeSVG()");
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
// Convert the canvas to an image of the correct MIME type
+1 -1
View File
@@ -103,7 +103,7 @@ export function createEditor() {
instance.openDocumentFile(filename, content);
// Remove the hash fragment from the URL
window.location.hash = "";
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
} catch {}
})();
+13 -7
View File
@@ -425,13 +425,10 @@ export class UpdateActiveDocument extends JsMessage {
export class DisplayDialogPanic extends JsMessage {
readonly panicInfo!: string;
readonly header!: string;
readonly description!: string;
}
export class DisplayDialog extends JsMessage {
readonly title!: string;
readonly icon!: IconName;
}
@@ -1049,6 +1046,8 @@ export class TextButton extends WidgetProps {
emphasized!: boolean;
noBackground!: boolean;
minWidth!: number;
disabled!: boolean;
@@ -1066,6 +1065,7 @@ export type TextButtonWidget = {
label: string;
icon?: IconName;
emphasized?: boolean;
noBackground?: boolean;
minWidth?: number;
disabled?: boolean;
tooltip?: string;
@@ -1222,7 +1222,7 @@ export function defaultWidgetLayout(): WidgetLayout {
}
// Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument
export function patchWidgetLayout(/* mut */ layout: WidgetLayout, updates: WidgetDiffUpdate): void {
export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: WidgetDiffUpdate): void {
layout.layoutTarget = updates.layoutTarget;
updates.diff.forEach((update) => {
@@ -1320,7 +1320,11 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
}
// WIDGET LAYOUTS
export class UpdateDialogDetails extends WidgetDiffUpdate { }
export class UpdateDialogButtons extends WidgetDiffUpdate { }
export class UpdateDialogColumn1 extends WidgetDiffUpdate { }
export class UpdateDialogColumn2 extends WidgetDiffUpdate { }
export class UpdateDocumentBarLayout extends WidgetDiffUpdate { }
@@ -1407,7 +1411,9 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerViewportResize,
TriggerVisitLink,
UpdateActiveDocument,
UpdateDialogDetails,
UpdateDialogButtons,
UpdateDialogColumn1,
UpdateDialogColumn2,
UpdateDocumentArtboards,
UpdateDocumentArtwork,
UpdateDocumentBarLayout,
+5 -2
View File
@@ -354,8 +354,11 @@ impl JsEditorHandle {
}
#[wasm_bindgen(js_name = requestAboutGraphiteDialogWithLocalizedCommitDate)]
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String) {
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date };
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String, localized_commit_year: String) {
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
localized_commit_date,
localized_commit_year,
};
self.dispatch(message);
}
+4 -15
View File
@@ -8,24 +8,13 @@ use wasm_bindgen::prelude::*;
/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
pub fn panic_hook(info: &panic::PanicInfo) {
let header = "The editor crashed — sorry about that";
let description = "
An internal error occurred. Please report this by filing an issue on GitHub.\n\
\n\
Reload the editor to continue. If this happens immediately on repeated reloads, clear saved data.
"
.trim();
error!("{}", info);
JS_EDITOR_HANDLES.with(|instances| {
instances.borrow_mut().values_mut().for_each(|instance| {
instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic {
panic_info: info.to_string(),
header: header.to_string(),
description: description.to_string(),
})
})
instances
.borrow_mut()
.values_mut()
.for_each(|instance| instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }))
});
}