Lint and so on
This commit is contained in:
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["svelte.svelte-vscode"]
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ If you have state that's important to retain within a component, consider creati
|
|||||||
```ts
|
```ts
|
||||||
// store.ts
|
// store.ts
|
||||||
// An extremely simple external store
|
// An extremely simple external store
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store';
|
||||||
export default writable(0)
|
export default writable(0);
|
||||||
```
|
```
|
||||||
|
|||||||
40
eslint.config.js
Normal file
40
eslint.config.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, ...globals.node },
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte', '**/*.svelte.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser,
|
||||||
|
extraFileExtensions: ['.svelte'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: ts.parser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ ignores: ['dist/'] },
|
||||||
|
);
|
||||||
20
index.html
20
index.html
@@ -1,13 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>buboard</title>
|
<title>buboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
87
package.json
87
package.json
@@ -1,41 +1,50 @@
|
|||||||
{
|
{
|
||||||
"name": "buboard",
|
"name": "buboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
||||||
},
|
"lint": "eslint .",
|
||||||
"devDependencies": {
|
"format": "prettier --write .",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"format:check": "prettier --check ."
|
||||||
"@tsconfig/svelte": "^5.0.6",
|
},
|
||||||
"@types/node": "^24.10.1",
|
"devDependencies": {
|
||||||
"svelte": "^5.43.8",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"svelte-check": "^4.3.4",
|
"@tsconfig/svelte": "^5.0.6",
|
||||||
"typescript": "~5.9.3",
|
"@types/node": "^24.10.1",
|
||||||
"vite": "npm:rolldown-vite@7.2.5"
|
"eslint": "^9.39.1",
|
||||||
},
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"pnpm": {
|
"eslint-plugin-svelte": "^3.13.0",
|
||||||
"overrides": {
|
"globals": "^16.5.0",
|
||||||
"vite": "npm:rolldown-vite@7.2.5"
|
"prettier": "^3.7.4",
|
||||||
}
|
"svelte": "^5.43.8",
|
||||||
},
|
"svelte-check": "^4.3.4",
|
||||||
"dependencies": {
|
"typescript": "~5.9.3",
|
||||||
"@codemirror/commands": "^6.10.0",
|
"typescript-eslint": "^8.48.1",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"vite": "npm:rolldown-vite@7.2.5"
|
||||||
"@codemirror/lang-html": "^6.4.11",
|
},
|
||||||
"@codemirror/language": "^6.11.3",
|
"pnpm": {
|
||||||
"@codemirror/state": "^6.5.2",
|
"overrides": {
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"vite": "npm:rolldown-vite@7.2.5"
|
||||||
"@codemirror/view": "^6.38.8",
|
}
|
||||||
"@lezer/highlight": "^1.2.3",
|
},
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"dependencies": {
|
||||||
"@replit/codemirror-vim": "^6.3.0",
|
"@codemirror/commands": "^6.10.0",
|
||||||
"codemirror": "^6.0.2",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"jszip": "^3.10.1",
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
"lucide-svelte": "^0.555.0"
|
"@codemirror/language": "^6.11.3",
|
||||||
}
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.38.8",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"lucide-svelte": "^0.555.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
997
pnpm-lock.yaml
generated
997
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
{@html `<style>${appState.manifest.appCss}</style>`}
|
{@html `<style>${appState.manifest.appCss}</style>`}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
<button class="show-ui" onclick={() => (interfaceHidden = false)} title="Show interface">
|
<button class="show-ui" onclick={() => (interfaceHidden = false)} title="Show interface">
|
||||||
<Eye size={14} />
|
<Eye size={14} />
|
||||||
</button>
|
</button>
|
||||||
{#each ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as key}
|
{#each ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as key (key)}
|
||||||
<button
|
<button
|
||||||
class="flag"
|
class="flag"
|
||||||
class:filled={appState.hasFlag(key)}
|
class:filled={appState.hasFlag(key)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Departure Mono';
|
font-family: 'Departure Mono';
|
||||||
src: url('/fonts/DepartureMono-Regular.woff2') format('woff2'),
|
src:
|
||||||
|
url('/fonts/DepartureMono-Regular.woff2') format('woff2'),
|
||||||
url('/fonts/DepartureMono-Regular.woff') format('woff');
|
url('/fonts/DepartureMono-Regular.woff') format('woff');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
import type { Item } from './types';
|
import type { Item } from './types';
|
||||||
import { state as appState } from './state.svelte';
|
import { state as appState } from './state.svelte';
|
||||||
import { calculateCenterOffset, constrainToAspectRatio } from './geometry';
|
import { calculateCenterOffset, constrainToAspectRatio } from './geometry';
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
let isResizing = $state(false);
|
let isResizing = $state(false);
|
||||||
let isRotating = $state(false);
|
let isRotating = $state(false);
|
||||||
let dragStart = { x: 0, y: 0, itemX: 0, itemY: 0 };
|
let dragStart = { x: 0, y: 0, itemX: 0, itemY: 0 };
|
||||||
let dragStartPositions: Map<string, { x: number; y: number }> = new Map();
|
let dragStartPositions: SvelteMap<string, { x: number; y: number }> = new SvelteMap();
|
||||||
let resizeStart = { x: 0, y: 0, width: 0, height: 0, itemX: 0, itemY: 0, corner: '', aspectRatio: 1 };
|
let resizeStart = { x: 0, y: 0, width: 0, height: 0, itemX: 0, itemY: 0, corner: '', aspectRatio: 1 };
|
||||||
let rotateStart = { angle: 0, startAngle: 0 };
|
let rotateStart = { angle: 0, startAngle: 0 };
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
itemX: item.x,
|
itemX: item.x,
|
||||||
itemY: item.y
|
itemY: item.y
|
||||||
};
|
};
|
||||||
dragStartPositions = new Map();
|
dragStartPositions = new SvelteMap();
|
||||||
for (const id of appState.selectedIds) {
|
for (const id of appState.selectedIds) {
|
||||||
const selectedItem = appState.getItem(id);
|
const selectedItem = appState.getItem(id);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
const x = -state.viewport.x / state.viewport.zoom + 400;
|
const x = -state.viewport.x / state.viewport.zoom + 400;
|
||||||
const y = -state.viewport.y / state.viewport.zoom + 300;
|
const y = -state.viewport.y / state.viewport.zoom + 300;
|
||||||
|
|
||||||
const img = new Image();
|
const img = document.createElement('img');
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
state.addAsset(assetId, { blob: file, url, filename: file.name });
|
state.addAsset(assetId, { blob: file, url, filename: file.name });
|
||||||
state.addItem({
|
state.addItem({
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<span class="app-name">Buboard</span>
|
<span class="app-name">Buboard</span>
|
||||||
<Palette />
|
<Palette />
|
||||||
<div class="flags">
|
<div class="flags">
|
||||||
{#each flagKeys as key}
|
{#each flagKeys as key (key)}
|
||||||
<button
|
<button
|
||||||
class="flag"
|
class="flag"
|
||||||
class:filled={state.hasFlag(key)}
|
class:filled={state.hasFlag(key)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function calculateCenterOffset(
|
|||||||
corner: string,
|
corner: string,
|
||||||
deltaWidth: number,
|
deltaWidth: number,
|
||||||
deltaHeight: number,
|
deltaHeight: number,
|
||||||
rotation: number
|
rotation: number,
|
||||||
): Point {
|
): Point {
|
||||||
const rad = (rotation * Math.PI) / 180;
|
const rad = (rotation * Math.PI) / 180;
|
||||||
const cos = Math.cos(rad);
|
const cos = Math.cos(rad);
|
||||||
@@ -24,14 +24,14 @@ export function calculateCenterOffset(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
x: localDx * cos - localDy * sin,
|
x: localDx * cos - localDy * sin,
|
||||||
y: localDx * sin + localDy * cos
|
y: localDx * sin + localDy * cos,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function constrainToAspectRatio(
|
export function constrainToAspectRatio(
|
||||||
newWidth: number,
|
newWidth: number,
|
||||||
newHeight: number,
|
newHeight: number,
|
||||||
aspectRatio: number
|
aspectRatio: number,
|
||||||
): { width: number; height: number } {
|
): { width: number; height: number } {
|
||||||
const newRatio = newWidth / newHeight;
|
const newRatio = newWidth / newHeight;
|
||||||
|
|
||||||
@@ -47,13 +47,13 @@ export function detectRotationCorner(
|
|||||||
localY: number,
|
localY: number,
|
||||||
halfWidth: number,
|
halfWidth: number,
|
||||||
halfHeight: number,
|
halfHeight: number,
|
||||||
zoneRadius: number
|
zoneRadius: number,
|
||||||
): string | null {
|
): string | null {
|
||||||
const corners: Record<string, Point> = {
|
const corners: Record<string, Point> = {
|
||||||
nw: { x: -halfWidth, y: -halfHeight },
|
nw: { x: -halfWidth, y: -halfHeight },
|
||||||
ne: { x: halfWidth, y: -halfHeight },
|
ne: { x: halfWidth, y: -halfHeight },
|
||||||
sw: { x: -halfWidth, y: halfHeight },
|
sw: { x: -halfWidth, y: halfHeight },
|
||||||
se: { x: halfWidth, y: halfHeight }
|
se: { x: halfWidth, y: halfHeight },
|
||||||
};
|
};
|
||||||
|
|
||||||
const isInsideBounds =
|
const isInsideBounds =
|
||||||
@@ -71,8 +71,10 @@ export function detectRotationCorner(
|
|||||||
|
|
||||||
if (dist > zoneRadius || dist < 3) continue;
|
if (dist > zoneRadius || dist < 3) continue;
|
||||||
|
|
||||||
const isOutwardX = (name.includes('w') && dx < 0) || (name.includes('e') && dx > 0);
|
const isOutwardX =
|
||||||
const isOutwardY = (name.includes('n') && dy < 0) || (name.includes('s') && dy > 0);
|
(name.includes('w') && dx < 0) || (name.includes('e') && dx > 0);
|
||||||
|
const isOutwardY =
|
||||||
|
(name.includes('n') && dy < 0) || (name.includes('s') && dy > 0);
|
||||||
|
|
||||||
if (isOutwardX || isOutwardY) return name;
|
if (isOutwardX || isOutwardY) return name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import JSZip from 'jszip';
|
|||||||
import type { Manifest, AssetStore } from './types';
|
import type { Manifest, AssetStore } from './types';
|
||||||
import { state } from './state.svelte';
|
import { state } from './state.svelte';
|
||||||
|
|
||||||
export async function exportBoard(): Promise<{ success: boolean; error?: string }> {
|
export async function exportBoard(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const assetsFolder = zip.folder('assets');
|
const assetsFolder = zip.folder('assets');
|
||||||
@@ -12,7 +15,7 @@ export async function exportBoard(): Promise<{ success: boolean; error?: string
|
|||||||
version: 1,
|
version: 1,
|
||||||
items: state.manifest.items.map((item) => ({ ...item })),
|
items: state.manifest.items.map((item) => ({ ...item })),
|
||||||
sharedCss: state.manifest.sharedCss,
|
sharedCss: state.manifest.sharedCss,
|
||||||
appCss: state.manifest.appCss
|
appCss: state.manifest.appCss,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const item of exportManifest.items) {
|
for (const item of exportManifest.items) {
|
||||||
@@ -31,7 +34,7 @@ export async function exportBoard(): Promise<{ success: boolean; error?: string
|
|||||||
const blob = await zip.generateAsync({
|
const blob = await zip.generateAsync({
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
compression: 'DEFLATE',
|
compression: 'DEFLATE',
|
||||||
compressionOptions: { level: 9 }
|
compressionOptions: { level: 9 },
|
||||||
});
|
});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -41,28 +44,35 @@ export async function exportBoard(): Promise<{ success: boolean; error?: string
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, error: e instanceof Error ? e.message : 'Export failed' };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : 'Export failed',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importBoard(file: File): Promise<{ success: boolean; error?: string }> {
|
export async function importBoard(
|
||||||
|
file: File,
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const zip = await JSZip.loadAsync(file);
|
const zip = await JSZip.loadAsync(file);
|
||||||
|
|
||||||
const manifestFile = zip.file('manifest.json');
|
const manifestFile = zip.file('manifest.json');
|
||||||
if (!manifestFile) throw new Error('Invalid .bub file: missing manifest.json');
|
if (!manifestFile)
|
||||||
|
throw new Error('Invalid .bub file: missing manifest.json');
|
||||||
|
|
||||||
const manifestJson = await manifestFile.async('string');
|
const manifestJson = await manifestFile.async('string');
|
||||||
const raw = JSON.parse(manifestJson);
|
const raw = JSON.parse(manifestJson);
|
||||||
|
|
||||||
if (raw.version !== 1) throw new Error(`Unsupported manifest version: ${raw.version}`);
|
if (raw.version !== 1)
|
||||||
|
throw new Error(`Unsupported manifest version: ${raw.version}`);
|
||||||
|
|
||||||
const manifest: Manifest = {
|
const manifest: Manifest = {
|
||||||
version: 1,
|
version: 1,
|
||||||
items: raw.items,
|
items: raw.items,
|
||||||
sharedCss: raw.sharedCss ?? '',
|
sharedCss: raw.sharedCss ?? '',
|
||||||
appCss: raw.appCss ?? '',
|
appCss: raw.appCss ?? '',
|
||||||
flags: raw.flags ?? {}
|
flags: raw.flags ?? {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const assets: AssetStore = {};
|
const assets: AssetStore = {};
|
||||||
@@ -70,7 +80,9 @@ export async function importBoard(file: File): Promise<{ success: boolean; error
|
|||||||
|
|
||||||
for (const item of manifest.items) {
|
for (const item of manifest.items) {
|
||||||
if (item.assetId) {
|
if (item.assetId) {
|
||||||
const assetFiles = zip.folder('assets')?.file(new RegExp(`^${item.assetId}\\.`));
|
const assetFiles = zip
|
||||||
|
.folder('assets')
|
||||||
|
?.file(new RegExp(`^${item.assetId}\\.`));
|
||||||
if (assetFiles && assetFiles.length > 0) {
|
if (assetFiles && assetFiles.length > 0) {
|
||||||
const assetFile = assetFiles[0];
|
const assetFile = assetFiles[0];
|
||||||
const blob = await assetFile.async('blob');
|
const blob = await assetFile.async('blob');
|
||||||
@@ -92,6 +104,9 @@ export async function importBoard(file: File): Promise<{ success: boolean; error
|
|||||||
state.load(manifest, assets);
|
state.load(manifest, assets);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, error: e instanceof Error ? e.message : 'Import failed' };
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : 'Import failed',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Item, Manifest, Asset, AssetStore, Viewport, PositionFlag } from './types';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
import type { Item, Manifest, Asset, AssetStore, Viewport } from './types';
|
||||||
|
|
||||||
const STORAGE_KEY = 'buboard';
|
const STORAGE_KEY = 'buboard';
|
||||||
|
|
||||||
@@ -68,11 +69,11 @@ function createState() {
|
|||||||
items: [],
|
items: [],
|
||||||
sharedCss: DEFAULT_SHARED_CSS,
|
sharedCss: DEFAULT_SHARED_CSS,
|
||||||
appCss: DEFAULT_APP_CSS,
|
appCss: DEFAULT_APP_CSS,
|
||||||
flags: {}
|
flags: {},
|
||||||
});
|
});
|
||||||
let assets = $state<AssetStore>({});
|
let assets = $state<AssetStore>({});
|
||||||
let viewport = $state<Viewport>({ x: 0, y: 0, zoom: 1 });
|
let viewport = $state<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||||
let selectedIds = $state<Set<string>>(new Set());
|
let selectedIds = new SvelteSet<string>();
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let editingGlobal = $state<boolean>(false);
|
let editingGlobal = $state<boolean>(false);
|
||||||
let focusedId = $state<string | null>(null);
|
let focusedId = $state<string | null>(null);
|
||||||
@@ -81,8 +82,10 @@ function createState() {
|
|||||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let animationId: number | null = null;
|
let animationId: number | null = null;
|
||||||
|
|
||||||
let maxZIndex = $derived(
|
const maxZIndex = $derived(
|
||||||
manifest.items.length > 0 ? Math.max(...manifest.items.map((i) => i.zIndex)) : 0
|
manifest.items.length > 0
|
||||||
|
? Math.max(...manifest.items.map((i) => i.zIndex))
|
||||||
|
: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -92,7 +95,7 @@ function createState() {
|
|||||||
for (const [id, asset] of Object.entries(assets)) {
|
for (const [id, asset] of Object.entries(assets)) {
|
||||||
storedAssets[id] = {
|
storedAssets[id] = {
|
||||||
dataUrl: await blobToDataUrl(asset.blob),
|
dataUrl: await blobToDataUrl(asset.blob),
|
||||||
filename: asset.filename
|
filename: asset.filename,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const stored: StoredState = { manifest, assets: storedAssets };
|
const stored: StoredState = { manifest, assets: storedAssets };
|
||||||
@@ -168,11 +171,11 @@ function createState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function select(id: string | null) {
|
function select(id: string | null) {
|
||||||
selectedIds = new Set(id ? [id] : []);
|
selectedIds = new SvelteSet(id ? [id] : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelection(id: string) {
|
function toggleSelection(id: string) {
|
||||||
const newSet = new Set(selectedIds);
|
const newSet = new SvelteSet(selectedIds);
|
||||||
if (newSet.has(id)) {
|
if (newSet.has(id)) {
|
||||||
newSet.delete(id);
|
newSet.delete(id);
|
||||||
} else {
|
} else {
|
||||||
@@ -182,7 +185,7 @@ function createState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selectedIds = new Set();
|
selectedIds = new SvelteSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(id: string | null) {
|
function edit(id: string | null) {
|
||||||
@@ -219,19 +222,27 @@ function createState() {
|
|||||||
function copySelected() {
|
function copySelected() {
|
||||||
if (selectedIds.size === 0) return;
|
if (selectedIds.size === 0) return;
|
||||||
const items = manifest.items.filter((i) => selectedIds.has(i.id));
|
const items = manifest.items.filter((i) => selectedIds.has(i.id));
|
||||||
clipboard = items.map(({ id, ...rest }) => rest);
|
clipboard = items.map(({ id: _id, ...rest }) => rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pasteItems(x: number, y: number): string[] {
|
function pasteItems(x: number, y: number): string[] {
|
||||||
if (clipboard.length === 0) return [];
|
if (clipboard.length === 0) return [];
|
||||||
const newIds: string[] = [];
|
const newIds: string[] = [];
|
||||||
const centerX = clipboard.reduce((sum, i) => sum + i.x, 0) / clipboard.length;
|
const centerX =
|
||||||
const centerY = clipboard.reduce((sum, i) => sum + i.y, 0) / clipboard.length;
|
clipboard.reduce((sum, i) => sum + i.x, 0) / clipboard.length;
|
||||||
|
const centerY =
|
||||||
|
clipboard.reduce((sum, i) => sum + i.y, 0) / clipboard.length;
|
||||||
for (const item of clipboard) {
|
for (const item of clipboard) {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const offsetX = item.x - centerX;
|
const offsetX = item.x - centerX;
|
||||||
const offsetY = item.y - centerY;
|
const offsetY = item.y - centerY;
|
||||||
addItem({ ...item, id, x: x + offsetX, y: y + offsetY, zIndex: maxZIndex + 1 });
|
addItem({
|
||||||
|
...item,
|
||||||
|
id,
|
||||||
|
x: x + offsetX,
|
||||||
|
y: y + offsetY,
|
||||||
|
zIndex: maxZIndex + 1,
|
||||||
|
});
|
||||||
newIds.push(id);
|
newIds.push(id);
|
||||||
}
|
}
|
||||||
return newIds;
|
return newIds;
|
||||||
@@ -316,7 +327,11 @@ function createState() {
|
|||||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateViewport(targetX: number, targetY: number, targetZoom: number) {
|
function animateViewport(
|
||||||
|
targetX: number,
|
||||||
|
targetY: number,
|
||||||
|
targetZoom: number,
|
||||||
|
) {
|
||||||
if (animationId) cancelAnimationFrame(animationId);
|
if (animationId) cancelAnimationFrame(animationId);
|
||||||
|
|
||||||
const startX = viewport.x;
|
const startX = viewport.x;
|
||||||
@@ -358,11 +373,11 @@ function createState() {
|
|||||||
items: [],
|
items: [],
|
||||||
sharedCss: DEFAULT_SHARED_CSS,
|
sharedCss: DEFAULT_SHARED_CSS,
|
||||||
appCss: DEFAULT_APP_CSS,
|
appCss: DEFAULT_APP_CSS,
|
||||||
flags: {}
|
flags: {},
|
||||||
};
|
};
|
||||||
assets = {};
|
assets = {};
|
||||||
viewport = { x: 0, y: 0, zoom: 1 };
|
viewport = { x: 0, y: 0, zoom: 1 };
|
||||||
selectedIds = new Set();
|
selectedIds = new SvelteSet();
|
||||||
editingId = null;
|
editingId = null;
|
||||||
editingGlobal = false;
|
editingGlobal = false;
|
||||||
focusedId = null;
|
focusedId = null;
|
||||||
@@ -374,7 +389,7 @@ function createState() {
|
|||||||
manifest = newManifest;
|
manifest = newManifest;
|
||||||
assets = newAssets;
|
assets = newAssets;
|
||||||
viewport = { x: 0, y: 0, zoom: 1 };
|
viewport = { x: 0, y: 0, zoom: 1 };
|
||||||
selectedIds = new Set();
|
selectedIds = new SvelteSet();
|
||||||
editingId = null;
|
editingId = null;
|
||||||
editingGlobal = false;
|
editingGlobal = false;
|
||||||
focusedId = null;
|
focusedId = null;
|
||||||
@@ -438,7 +453,7 @@ function createState() {
|
|||||||
clearFlag,
|
clearFlag,
|
||||||
gotoFlag,
|
gotoFlag,
|
||||||
reset,
|
reset,
|
||||||
load
|
load,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
src/lib/theme.ts
105
src/lib/theme.ts
@@ -3,7 +3,9 @@ import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
|
|||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
|
|
||||||
function getVar(name: string, fallback: string): string {
|
function getVar(name: string, fallback: string): string {
|
||||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
const value = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue(name)
|
||||||
|
.trim();
|
||||||
return value || fallback;
|
return value || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,104 +29,145 @@ export function createTheme() {
|
|||||||
'&': {
|
'&': {
|
||||||
backgroundColor: surface,
|
backgroundColor: surface,
|
||||||
color: '#abb2bf',
|
color: '#abb2bf',
|
||||||
height: '100%'
|
height: '100%',
|
||||||
},
|
},
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
fontFamily: "'Departure Mono', monospace"
|
fontFamily: "'Departure Mono', monospace",
|
||||||
},
|
},
|
||||||
'.cm-content': {
|
'.cm-content': {
|
||||||
caretColor: accent
|
caretColor: accent,
|
||||||
},
|
},
|
||||||
'.cm-cursor, .cm-dropCursor': {
|
'.cm-cursor, .cm-dropCursor': {
|
||||||
borderLeftColor: accent
|
borderLeftColor: accent,
|
||||||
},
|
},
|
||||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
||||||
{
|
{
|
||||||
backgroundColor: '#3E4451'
|
backgroundColor: '#3E4451',
|
||||||
},
|
},
|
||||||
'.cm-panels': {
|
'.cm-panels': {
|
||||||
backgroundColor: surface,
|
backgroundColor: surface,
|
||||||
color: '#abb2bf'
|
color: '#abb2bf',
|
||||||
},
|
},
|
||||||
'.cm-panels.cm-panels-top': {
|
'.cm-panels.cm-panels-top': {
|
||||||
borderBottom: `1px solid ${border}`
|
borderBottom: `1px solid ${border}`,
|
||||||
},
|
},
|
||||||
'.cm-panels.cm-panels-bottom': {
|
'.cm-panels.cm-panels-bottom': {
|
||||||
borderTop: `1px solid ${border}`
|
borderTop: `1px solid ${border}`,
|
||||||
},
|
},
|
||||||
'.cm-searchMatch': {
|
'.cm-searchMatch': {
|
||||||
backgroundColor: '#72a1ff59',
|
backgroundColor: '#72a1ff59',
|
||||||
outline: `1px solid ${border}`
|
outline: `1px solid ${border}`,
|
||||||
},
|
},
|
||||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||||
backgroundColor: '#6199ff2f'
|
backgroundColor: '#6199ff2f',
|
||||||
},
|
},
|
||||||
'.cm-activeLine': {
|
'.cm-activeLine': {
|
||||||
backgroundColor: '#2c313c50'
|
backgroundColor: '#2c313c50',
|
||||||
},
|
},
|
||||||
'.cm-selectionMatch': {
|
'.cm-selectionMatch': {
|
||||||
backgroundColor: '#aafe661a'
|
backgroundColor: '#aafe661a',
|
||||||
},
|
},
|
||||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||||
backgroundColor: '#bad0f847'
|
backgroundColor: '#bad0f847',
|
||||||
},
|
},
|
||||||
'.cm-gutters': {
|
'.cm-gutters': {
|
||||||
backgroundColor: surface,
|
backgroundColor: surface,
|
||||||
color: textDim,
|
color: textDim,
|
||||||
border: 'none'
|
border: 'none',
|
||||||
},
|
},
|
||||||
'.cm-activeLineGutter': {
|
'.cm-activeLineGutter': {
|
||||||
backgroundColor: '#2c313c50'
|
backgroundColor: '#2c313c50',
|
||||||
},
|
},
|
||||||
'.cm-foldPlaceholder': {
|
'.cm-foldPlaceholder': {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: textDim
|
color: textDim,
|
||||||
},
|
},
|
||||||
'.cm-tooltip': {
|
'.cm-tooltip': {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
backgroundColor: surface
|
backgroundColor: surface,
|
||||||
},
|
},
|
||||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||||
borderTopColor: 'transparent',
|
borderTopColor: 'transparent',
|
||||||
borderBottomColor: 'transparent'
|
borderBottomColor: 'transparent',
|
||||||
},
|
},
|
||||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||||
borderTopColor: surface,
|
borderTopColor: surface,
|
||||||
borderBottomColor: surface
|
borderBottomColor: surface,
|
||||||
},
|
},
|
||||||
'.cm-tooltip-autocomplete': {
|
'.cm-tooltip-autocomplete': {
|
||||||
'& > ul > li[aria-selected]': {
|
'& > ul > li[aria-selected]': {
|
||||||
backgroundColor: accent,
|
backgroundColor: accent,
|
||||||
color: text
|
color: text,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ dark: true }
|
{ dark: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const highlighting = HighlightStyle.define([
|
const highlighting = HighlightStyle.define([
|
||||||
{ tag: tags.keyword, color: keyword },
|
{ tag: tags.keyword, color: keyword },
|
||||||
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: variable },
|
{
|
||||||
|
tag: [
|
||||||
|
tags.name,
|
||||||
|
tags.deleted,
|
||||||
|
tags.character,
|
||||||
|
tags.propertyName,
|
||||||
|
tags.macroName,
|
||||||
|
],
|
||||||
|
color: variable,
|
||||||
|
},
|
||||||
{ tag: [tags.function(tags.variableName), tags.labelName], color: func },
|
{ tag: [tags.function(tags.variableName), tags.labelName], color: func },
|
||||||
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: number },
|
{
|
||||||
|
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
|
||||||
|
color: number,
|
||||||
|
},
|
||||||
{ tag: [tags.definition(tags.name), tags.separator], color: '#abb2bf' },
|
{ tag: [tags.definition(tags.name), tags.separator], color: '#abb2bf' },
|
||||||
{ tag: [tags.typeName, tags.className, tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: number },
|
{
|
||||||
{ tag: [tags.operator, tags.operatorKeyword, tags.url, tags.escape, tags.regexp, tags.link, tags.special(tags.string)], color: operator },
|
tag: [
|
||||||
|
tags.typeName,
|
||||||
|
tags.className,
|
||||||
|
tags.number,
|
||||||
|
tags.changed,
|
||||||
|
tags.annotation,
|
||||||
|
tags.modifier,
|
||||||
|
tags.self,
|
||||||
|
tags.namespace,
|
||||||
|
],
|
||||||
|
color: number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.operator,
|
||||||
|
tags.operatorKeyword,
|
||||||
|
tags.url,
|
||||||
|
tags.escape,
|
||||||
|
tags.regexp,
|
||||||
|
tags.link,
|
||||||
|
tags.special(tags.string),
|
||||||
|
],
|
||||||
|
color: operator,
|
||||||
|
},
|
||||||
{ tag: [tags.meta, tags.comment], color: comment, fontStyle: 'italic' },
|
{ tag: [tags.meta, tags.comment], color: comment, fontStyle: 'italic' },
|
||||||
{ tag: tags.strong, fontWeight: 'bold' },
|
{ tag: tags.strong, fontWeight: 'bold' },
|
||||||
{ tag: tags.emphasis, fontStyle: 'italic' },
|
{ tag: tags.emphasis, fontStyle: 'italic' },
|
||||||
{ tag: tags.strikethrough, textDecoration: 'line-through' },
|
{ tag: tags.strikethrough, textDecoration: 'line-through' },
|
||||||
{ tag: tags.link, color: comment, textDecoration: 'underline' },
|
{ tag: tags.link, color: comment, textDecoration: 'underline' },
|
||||||
{ tag: tags.heading, fontWeight: 'bold', color: variable },
|
{ tag: tags.heading, fontWeight: 'bold', color: variable },
|
||||||
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: number },
|
{
|
||||||
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: string },
|
tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
|
||||||
|
color: number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [tags.processingInstruction, tags.string, tags.inserted],
|
||||||
|
color: string,
|
||||||
|
},
|
||||||
{ tag: tags.invalid, color: '#ff0000' },
|
{ tag: tags.invalid, color: '#ff0000' },
|
||||||
{ tag: tags.tagName, color: variable },
|
{ tag: tags.tagName, color: variable },
|
||||||
{ tag: tags.attributeName, color: number },
|
{ tag: tags.attributeName, color: number },
|
||||||
{ tag: tags.attributeValue, color: string },
|
{ tag: tags.attributeValue, color: string },
|
||||||
{ tag: tags.propertyName, color: func }
|
{ tag: tags.propertyName, color: func },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [theme, syntaxHighlighting(highlighting)];
|
return [theme, syntaxHighlighting(highlighting)];
|
||||||
|
|||||||
12
src/main.ts
12
src/main.ts
@@ -1,9 +1,9 @@
|
|||||||
import { mount } from 'svelte'
|
import { mount } from 'svelte';
|
||||||
import './app.css'
|
import './app.css';
|
||||||
import App from './App.svelte'
|
import App from './App.svelte';
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = mount(App, {
|
||||||
target: document.getElementById('app')!,
|
target: document.getElementById('app')!,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default app
|
export default app;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
||||||
export default {
|
export default {
|
||||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["svelte", "vite/client"],
|
"types": ["svelte", "vite/client"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
/**
|
/**
|
||||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
* Note that setting allowJs false does not prevent the use
|
* Note that setting allowJs false does not prevent the use
|
||||||
* of JS in `.svelte` files.
|
* of JS in `.svelte` files.
|
||||||
*/
|
*/
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"moduleDetection": "force"
|
"moduleDetection": "force"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
{ "path": "./tsconfig.node.json" }
|
{ "path": "./tsconfig.node.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
"target": "ES2023",
|
"target": "ES2023",
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user