mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 00:38:12 +08:00
Replace the Vue frontend with Svelte
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { type IconName } from "@/utility-functions/icons";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
@@ -6,7 +6,7 @@ import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayD
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createDialogState(editor: Editor) {
|
||||
const state = reactive({
|
||||
const { subscribe, update } = writable({
|
||||
visible: false,
|
||||
icon: "" as IconName,
|
||||
widgets: defaultWidgetLayout(),
|
||||
@@ -15,37 +15,45 @@ export function createDialogState(editor: Editor) {
|
||||
});
|
||||
|
||||
function dismissDialog(): void {
|
||||
state.visible = false;
|
||||
}
|
||||
|
||||
function dialogIsVisible(): boolean {
|
||||
return state.visible;
|
||||
update((state) => {
|
||||
state.visible = false;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a panic dialog from JS.
|
||||
// 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 createPanicDialog(icon: IconName, widgets: WidgetLayout, jsCallbackBasedButtons: TextButtonWidget[]): void {
|
||||
state.visible = true;
|
||||
state.icon = icon;
|
||||
state.widgets = widgets;
|
||||
state.jsCallbackBasedButtons = jsCallbackBasedButtons;
|
||||
update((state) => {
|
||||
state.visible = true;
|
||||
state.icon = icon;
|
||||
state.widgets = widgets;
|
||||
state.jsCallbackBasedButtons = jsCallbackBasedButtons;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(DisplayDialog, (displayDialog) => {
|
||||
state.visible = true;
|
||||
state.icon = displayDialog.icon;
|
||||
update((state) => {
|
||||
state.visible = true;
|
||||
state.icon = displayDialog.icon;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
|
||||
patchWidgetLayout(state.widgets, updateDialogDetails);
|
||||
state.jsCallbackBasedButtons = undefined;
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.widgets, updateDialogDetails);
|
||||
|
||||
state.jsCallbackBasedButtons = undefined;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
subscribe,
|
||||
dismissDialog,
|
||||
dialogIsVisible,
|
||||
createPanicDialog,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { nextTick, reactive, readonly } from "vue";
|
||||
import {tick} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
defaultWidgetLayout,
|
||||
patchWidgetLayout,
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
@@ -13,7 +15,7 @@ import {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createDocumentState(editor: Editor) {
|
||||
const state = reactive({
|
||||
const state = writable({
|
||||
// Layouts
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
@@ -21,31 +23,66 @@ export function createDocumentState(editor: Editor) {
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
workingColorsLayout: defaultWidgetLayout(),
|
||||
});
|
||||
const { subscribe, update } = state;
|
||||
|
||||
// Update layouts
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentModeLayout, async (updateDocumentModeLayout) => {
|
||||
await nextTick();
|
||||
patchWidgetLayout(state.documentModeLayout, updateDocumentModeLayout);
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
// `state.documentModeLayout` is mutated in the function
|
||||
patchWidgetLayout(state.documentModeLayout, updateDocumentModeLayout);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolOptionsLayout, async (updateToolOptionsLayout) => {
|
||||
await nextTick();
|
||||
patchWidgetLayout(state.toolOptionsLayout, updateToolOptionsLayout);
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
// `state.documentModeLayout` is mutated in the function
|
||||
patchWidgetLayout(state.toolOptionsLayout, updateToolOptionsLayout);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentBarLayout, async (updateDocumentBarLayout) => {
|
||||
await nextTick();
|
||||
patchWidgetLayout(state.documentBarLayout, updateDocumentBarLayout);
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
// `state.documentModeLayout` is mutated in the function
|
||||
patchWidgetLayout(state.documentBarLayout, updateDocumentBarLayout);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolShelfLayout, async (updateToolShelfLayout) => {
|
||||
await nextTick();
|
||||
patchWidgetLayout(state.toolShelfLayout, updateToolShelfLayout);
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
// `state.documentModeLayout` is mutated in the function
|
||||
patchWidgetLayout(state.toolShelfLayout, updateToolShelfLayout);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateWorkingColorsLayout, async (updateWorkingColorsLayout) => {
|
||||
await nextTick();
|
||||
patchWidgetLayout(state.workingColorsLayout, updateWorkingColorsLayout);
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
// `state.documentModeLayout` is mutated in the function
|
||||
patchWidgetLayout(state.workingColorsLayout, updateWorkingColorsLayout);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerRefreshBoundsOfViewports, async () => {
|
||||
// Wait to display the unpopulated document panel (missing: tools, options bar content, scrollbar positioning, and canvas)
|
||||
await tick();
|
||||
// Wait to display the populated document panel
|
||||
await tick();
|
||||
|
||||
// Request a resize event so the viewport gets measured now that the canvas is populated and positioned correctly
|
||||
window.dispatchEvent(new CustomEvent("resize"));
|
||||
});
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
export type DocumentState = ReturnType<typeof createDocumentState>;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { reactive } from "vue";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import { TriggerFontLoad } from "@/wasm-communication/messages";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createFontsState(editor: Editor) {
|
||||
const state = reactive({});
|
||||
// TODO: Do some code cleanup to remove the need for this empty store
|
||||
const { subscribe } = writable({});
|
||||
|
||||
function createURL(font: string): URL {
|
||||
const url = new URL("https://fonts.googleapis.com/css2");
|
||||
@@ -73,7 +74,12 @@ export function createFontsState(editor: Editor) {
|
||||
});
|
||||
});
|
||||
|
||||
return { state, fontNames, getFontStyles, getFontFileUrl };
|
||||
return {
|
||||
subscribe,
|
||||
fontNames,
|
||||
getFontStyles,
|
||||
getFontFileUrl,
|
||||
};
|
||||
}
|
||||
export type FontsState = ReturnType<typeof createFontsState>;
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createFullscreenState(_: Editor) {
|
||||
const state = reactive({
|
||||
const { subscribe, update } = writable({
|
||||
windowFullscreen: false,
|
||||
keyboardLocked: false,
|
||||
});
|
||||
|
||||
function fullscreenModeChanged(): void {
|
||||
state.windowFullscreen = Boolean(document.fullscreenElement);
|
||||
if (!state.windowFullscreen) state.keyboardLocked = false;
|
||||
update((state) => {
|
||||
state.windowFullscreen = Boolean(document.fullscreenElement);
|
||||
if (!state.windowFullscreen) state.keyboardLocked = false;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
async function enterFullscreen(): Promise<void> {
|
||||
@@ -20,7 +23,11 @@ export function createFullscreenState(_: Editor) {
|
||||
if (keyboardLockApiSupported) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (navigator as any).keyboard.lock(["ControlLeft", "ControlRight"]);
|
||||
state.keyboardLocked = true;
|
||||
|
||||
update((state) => {
|
||||
state.keyboardLocked = true;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +36,14 @@ export function createFullscreenState(_: Editor) {
|
||||
}
|
||||
|
||||
async function toggleFullscreen(): Promise<void> {
|
||||
if (state.windowFullscreen) await exitFullscreen();
|
||||
else await enterFullscreen();
|
||||
return new Promise((resolve, reject) => {
|
||||
update((state) => {
|
||||
if (state.windowFullscreen) exitFullscreen().then(resolve).catch(reject);
|
||||
else enterFullscreen().then(resolve).catch(reject);
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard
|
||||
@@ -38,7 +51,7 @@ export function createFullscreenState(_: Editor) {
|
||||
const keyboardLockApiSupported: Readonly<boolean> = "keyboard" in navigator && (navigator as any).keyboard && "lock" in (navigator as any).keyboard;
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
subscribe,
|
||||
fullscreenModeChanged,
|
||||
enterFullscreen,
|
||||
exitFullscreen,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
import {tick} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createNodeGraphState(editor: Editor) {
|
||||
const state = reactive({
|
||||
const { subscribe, update } = writable({
|
||||
nodes: [] as FrontendNode[],
|
||||
links: [] as FrontendNodeLink[],
|
||||
nodeTypes: [] as FrontendNodeType[],
|
||||
@@ -25,21 +26,33 @@ export function createNodeGraphState(editor: Editor) {
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => {
|
||||
state.nodes = updateNodeGraph.nodes;
|
||||
state.links = updateNodeGraph.links;
|
||||
update((state) => {
|
||||
state.nodes = updateNodeGraph.nodes;
|
||||
state.links = updateNodeGraph.links;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => {
|
||||
state.nodeTypes = updateNodeTypes.nodeTypes;
|
||||
update((state) => {
|
||||
state.nodeTypes = updateNodeTypes.nodeTypes;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphBarLayout, (updateNodeGraphBarLayout) => {
|
||||
patchWidgetLayout(state.nodeGraphBarLayout, updateNodeGraphBarLayout);
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.nodeGraphBarLayout, updateNodeGraphBarLayout);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateZoomWithScroll, (updateZoomWithScroll) => {
|
||||
state.zoomWithScroll = updateZoomWithScroll.zoomWithScroll;
|
||||
update((state) => {
|
||||
state.zoomWithScroll = updateZoomWithScroll.zoomWithScroll;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
export type NodeGraphState = ReturnType<typeof createNodeGraphState>;
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { nextTick, reactive, readonly } from "vue";
|
||||
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
DisplayEditableTextbox,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerTextCommit,
|
||||
TriggerViewportResize,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentOverlays,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateMouseCursor,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
import DocumentComponent from "@/components/panels/Document.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createPanelsState(editor: Editor) {
|
||||
const state = reactive({
|
||||
documentPanel: DocumentComponent,
|
||||
});
|
||||
|
||||
// We use `any` instead of `typeof DocumentComponent` as a workaround for the fact that calling this function with the `this` argument from within `Document.vue` isn't a compatible type
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function registerPanel(type: string, panelComponent: any): void {
|
||||
state.documentPanel = panelComponent;
|
||||
}
|
||||
|
||||
function subscribeDocumentPanel(): void {
|
||||
// Update rendered SVGs
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (updateDocumentArtwork) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentArtwork(updateDocumentArtwork.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentOverlays, async (updateDocumentOverlays) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentOverlays(updateDocumentOverlays.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtboards, async (updateDocumentArtboards) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentArtboards(updateDocumentArtboards.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (updateEyedropperSamplingState) => {
|
||||
await nextTick();
|
||||
const { mousePosition, primaryColor, secondaryColor, setColorChoice } = updateEyedropperSamplingState;
|
||||
const rgb = (await state.documentPanel.updateEyedropperSamplingState(mousePosition, primaryColor, secondaryColor)) as [number, number, number] | undefined;
|
||||
|
||||
if (setColorChoice && rgb) {
|
||||
if (setColorChoice === "Primary") editor.instance.updatePrimaryColor(...rgb, 1);
|
||||
if (setColorChoice === "Secondary") editor.instance.updateSecondaryColor(...rgb, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Update scrollbars and rulers
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, async (updateDocumentScrollbars) => {
|
||||
await nextTick();
|
||||
const { position, size, multiplier } = updateDocumentScrollbars;
|
||||
state.documentPanel.updateDocumentScrollbars(position, size, multiplier);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentRulers, async (updateDocumentRulers) => {
|
||||
await nextTick();
|
||||
const { origin, spacing, interval } = updateDocumentRulers;
|
||||
state.documentPanel.updateDocumentRulers(origin, spacing, interval);
|
||||
});
|
||||
|
||||
// Update mouse cursor icon
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMouseCursor, async (updateMouseCursor) => {
|
||||
await nextTick();
|
||||
const { cursor } = updateMouseCursor;
|
||||
state.documentPanel.updateMouseCursor(cursor);
|
||||
});
|
||||
|
||||
// Text entry
|
||||
editor.subscriptions.subscribeJsMessage(TriggerTextCommit, async () => {
|
||||
await nextTick();
|
||||
state.documentPanel.triggerTextCommit();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextbox, async (displayEditableTextbox) => {
|
||||
await nextTick();
|
||||
state.documentPanel.displayEditableTextbox(displayEditableTextbox);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, async () => {
|
||||
await nextTick();
|
||||
state.documentPanel.displayRemoveEditableTextbox();
|
||||
});
|
||||
|
||||
// Resize elements to render the new viewport size
|
||||
editor.subscriptions.subscribeJsMessage(TriggerViewportResize, async () => {
|
||||
await nextTick();
|
||||
state.documentPanel.viewportResize();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerRefreshBoundsOfViewports, async () => {
|
||||
// Wait to display the unpopulated document panel (missing: tools, options bar content, scrollbar positioning, and canvas)
|
||||
await nextTick();
|
||||
// Wait to display the populated document panel
|
||||
await nextTick();
|
||||
|
||||
// Request a resize event so the viewport gets measured now that the canvas is populated and positioned correctly
|
||||
window.dispatchEvent(new CustomEvent("resize"));
|
||||
});
|
||||
}
|
||||
|
||||
subscribeDocumentPanel();
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
registerPanel,
|
||||
};
|
||||
}
|
||||
export type PanelsState = ReturnType<typeof createPanelsState>;
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
||||
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate, updateBackendImage } from "@/utility-functions/imaginate";
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createPortfolioState(editor: Editor) {
|
||||
const state = reactive({
|
||||
const { subscribe, update } = writable({
|
||||
unsaved: false,
|
||||
documents: [] as FrontendDocumentDetails[],
|
||||
activeDocumentIndex: 0,
|
||||
@@ -31,12 +32,18 @@ export function createPortfolioState(editor: Editor) {
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => {
|
||||
state.documents = updateOpenDocumentList.openDocuments;
|
||||
update((state) => {
|
||||
state.documents = updateOpenDocumentList.openDocuments;
|
||||
return state;
|
||||
})
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateActiveDocument, (updateActiveDocument) => {
|
||||
// Assume we receive a correct document id
|
||||
const activeId = state.documents.findIndex((doc) => doc.id === updateActiveDocument.documentId);
|
||||
state.activeDocumentIndex = activeId;
|
||||
update((state) => {
|
||||
// Assume we receive a correct document id
|
||||
const activeId = state.documents.findIndex((doc) => doc.id === updateActiveDocument.documentId);
|
||||
state.activeDocumentIndex = activeId;
|
||||
return state;
|
||||
})
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
||||
const extension = editor.instance.fileSaveSuffix();
|
||||
@@ -122,7 +129,7 @@ export function createPortfolioState(editor: Editor) {
|
||||
});
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
export type PortfolioState = ReturnType<typeof createPortfolioState>;
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { nextTick, reactive, readonly } from "vue";
|
||||
|
||||
import {tick} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import { UpdateNodeGraphVisibility } from "@/wasm-communication/messages";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createWorkspaceState(editor: Editor) {
|
||||
const state = reactive({
|
||||
const { subscribe, update } = writable({
|
||||
nodeGraphVisible: false,
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphVisibility, async (updateNodeGraphVisibility) => {
|
||||
state.nodeGraphVisible = updateNodeGraphVisibility.visible;
|
||||
update((state) => {
|
||||
state.nodeGraphVisible = updateNodeGraphVisibility.visible;
|
||||
return state;
|
||||
});
|
||||
|
||||
// Update the viewport bounds
|
||||
await nextTick();
|
||||
await tick();
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
export type WorkspaceState = ReturnType<typeof createWorkspaceState>;
|
||||
|
||||
Reference in New Issue
Block a user