Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ API_KEY=test

# Fs root dir read/write projects (leave empty to select current directory) (if you use relative path, it will be <cwd>/<FS_ROOT>)
FS_ROOT=
# Archive root dir read/write archive (leave empty to select current directory) (if you use relative path, it will be <cwd>/<ARCHIVE_ROOT>)
# Shouldn't be in FS_ROOT to avoid name conflicts
ARCHIVE_ROOT=

# Leaving this empty will generate a new unique random session secret at start
SESSION_SECRET=
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@
"prepare": "husky"
},
"dependencies": {
"@nanoforge-dev/schematics": "^2.1.3",
"bun": "catalog:core",
"class-transformer": "catalog:libs-back",
"class-validator": "catalog:libs-back",
"dotenv": "catalog:libs-back",
"micromatch": "catalog:libs-back",
"tar": "catalog:libs-back",
"typescript": "catalog:build"
},
"devDependencies": {
Expand All @@ -84,6 +85,7 @@
"@trivago/prettier-plugin-sort-imports": "catalog:lint",
"@tsconfig/svelte": "catalog:build",
"@types/bun": "catalog:core",
"@types/micromatch": "^4.0.10",
"@unocss/extractor-svelte": "catalog:css",
"@unocss/preset-icons": "catalog:css",
"@unocss/preset-web-fonts": "catalog:css",
Expand Down Expand Up @@ -119,7 +121,7 @@
"vitest-browser-svelte": "catalog:test",
"zod": "catalog:libs-front"
},
"packageManager": "pnpm@11.5.1",
"packageManager": "pnpm@11.6.0",
"engines": {
"node": "25"
},
Expand Down
1,378 changes: 703 additions & 675 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

30 changes: 16 additions & 14 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ catalogs:
'@commitlint/cli': ^21.0.2
'@commitlint/config-conventional': ^21.0.2
'@favware/cliff-jumper': ^6.1.0
'@nanoforge-dev/actions': ^2.1.3
'@nanoforge-dev/actions': ^2.1.4
husky: ^9.1.7
lint-staged: ^17.0.7
components:
Expand All @@ -20,38 +20,40 @@ catalogs:
core:
'@nanoforge-dev/ecs-lib': ^1.3.1
'@sveltejs/adapter-auto': ^7.0.1
'@sveltejs/kit': ^2.63.0
'@sveltejs/kit': ^2.65.0
'@sveltejs/vite-plugin-svelte': ^7.1.2
'@types/bun': ^1.3.14
bun: ^1.3.14
svelte: ^5.56.2
svelte: ^5.56.3
svelte-check: ^4.6.0
svelte-kit-sessions: ^0.4.0
vite: ^8.0.16
css:
'@alexanderniebuhr/prettier-plugin-unocss': ^0.0.4
'@unocss/extractor-svelte': ^66.7.0
'@unocss/preset-icons': ^66.7.0
'@unocss/preset-web-fonts': ^66.7.0
'@unocss/preset-wind4': ^66.7.0
'@unocss/extractor-svelte': ^66.7.2
'@unocss/preset-icons': ^66.7.2
'@unocss/preset-web-fonts': ^66.7.2
'@unocss/preset-wind4': ^66.7.2
clsx: ^2.1.1
tailwind-merge: ^3.6.0
tailwind-variants: ^3.2.2
tailwindcss: ^4.3.0
unocss: ^66.7.0
tailwindcss: ^4.3.1
unocss: ^66.7.2
i18n:
'@inlang/paraglide-js': ^2.18.2
'@inlang/paraglide-js': ^2.19.0
icons:
'@iconify-json/clarity': ^1.2.4
'@iconify-json/ic': ^1.2.4
'@iconify-json/icomoon-free': ^1.2.1
'@iconify-json/material-icon-theme': ^1.2.67
'@iconify-json/solar': ^1.2.5
'@lucide/svelte': ^1.17.0
'@lucide/svelte': ^1.18.0
libs-back:
class-transformer: ^0.5.1
class-validator: ^0.15.1
dotenv: ^17.4.2
micromatch: ^4.0.8
tar: ^7.5.16
libs-front:
'@internationalized/date': ^3.12.2
'@tanstack/svelte-query': ^6.1.34
Expand All @@ -64,12 +66,12 @@ catalogs:
'@nanoforge-dev/utils-eslint-config': ^1.0.2
'@nanoforge-dev/utils-prettier-config': ^1.0.2
'@trivago/prettier-plugin-sort-imports': ^6.0.2
eslint: ^10.4.1
eslint: ^10.5.0
eslint-plugin-svelte: ^3.19.0
globals: ^17.6.0
prettier: ^3.8.3
prettier: ^3.8.4
prettier-plugin-svelte: ^4.1.0
typescript-eslint: ^8.60.1
typescript-eslint: ^8.61.0
test:
'@playwright/test': ^1.60.0
'@vitest/browser-playwright': ^4.1.8
Expand Down
3 changes: 3 additions & 0 deletions src/lib/client/action/client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { SESSION_PROJECT_HEADER } from '@utils/const';
import { HttpClient } from '@utils/http';

import { ProjectArchiveRepository } from './repositories/archive.repository';
import { ProjectFsRepository } from './repositories/fs.repository';
import { ProjectLoaderRepository } from './repositories/loader.repository';
import { ProjectPackageRepository } from './repositories/package.repository';
import { ProjectRepository } from './repositories/project.repository';
import { ProjectSaveRepository } from './repositories/save.repository';

export interface ActionClient {
archive: ProjectArchiveRepository;
fs: ProjectFsRepository;
loader: ProjectLoaderRepository;
package: ProjectPackageRepository;
Expand All @@ -22,6 +24,7 @@ export const getActionClient = (projectId?: string): ActionClient => {
);

return {
archive: new ProjectArchiveRepository(client),
fs: new ProjectFsRepository(client),
loader: new ProjectLoaderRepository(client),
package: new ProjectPackageRepository(client),
Expand Down
8 changes: 8 additions & 0 deletions src/lib/client/action/repositories/archive.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BaseRepository } from '../base.repository';
import type { Archive } from '../types';

export class ProjectArchiveRepository extends BaseRepository {
create(): Promise<Archive> {
return this.run(`/actions/project/archive?/create`);
}
}
3 changes: 3 additions & 0 deletions src/lib/client/action/types/archive.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { CreateArchiveResult } from '$lib/server/actions/project/archive/create.action';

export type Archive = CreateArchiveResult;
1 change: 1 addition & 0 deletions src/lib/client/action/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './archive.type';
export * from './fs.type';
export * from './library.type';
export * from './loader.type';
Expand Down
74 changes: 45 additions & 29 deletions src/lib/components/Menu/MenuBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import type { Snippet } from 'svelte';
import { ProjectLoader } from '$lib/client/project';
import { ProjectLoader, useProject } from '$lib/client/project';
import { PUBLIC_DOCS_URL, PUBLIC_LANDING_URL } from '$env/static/public';
import ExportDialog from './export-dialog.svelte';

let fileInput: HTMLInputElement;
// let fileInput: HTMLInputElement;

type MenuItem = { icon: string } & (
| {
Expand All @@ -24,19 +25,37 @@
items: MenuItem[];
}

async function handleImportClick() {
fileInput.click();
}
const { actions } = useProject();

async function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
if (!file.name.endsWith('.zip')) return;
// async function handleImportClick() {
// fileInput.click();
// }

//await importFromZip(file);
input.value = '';
}
// async function handleFileChange(event: Event) {
// const input = event.target as HTMLInputElement;
// const file = input.files?.[0];
// if (!file) return;
// if (!file.name.endsWith('.zip')) return;
//
// //await importFromZip(file);
// input.value = '';
// }

let exportOpen = $state(false);
let exportLoading = $state(false);
let exportArchiveId = $state<string | null>(null);

const handleExportClick = async () => {
exportArchiveId = null;
exportLoading = true;
exportOpen = true;
try {
const { id } = await actions.archive.create();
exportArchiveId = id;
} finally {
exportLoading = false;
}
};

const nullFunction = () => {};

Expand All @@ -45,12 +64,7 @@
name: 'File',
items: [
{ name: 'Save', icon: 'i-solar-cloud-download-bold-duotone', onClick: nullFunction },
{
snippet: fileImportSnippet,
icon: 'i-solar-download-bold-duotone',
onClick: handleImportClick,
},
{ name: 'Export', icon: 'i-solar-file-send-bold-duotone', onClick: nullFunction },
{ name: 'Export', icon: 'i-solar-file-send-bold-duotone', onClick: handleExportClick },
{
name: 'Exit',
icon: 'i-solar-exit-bold-duotone',
Expand Down Expand Up @@ -83,16 +97,16 @@
];
</script>

{#snippet fileImportSnippet()}
Import
<input
type="file"
accept=".zip"
bind:this={fileInput}
class="hidden"
on:change={handleFileChange}
/>
{/snippet}
<!--{#snippet fileImportSnippet()}-->
<!-- Import-->
<!-- <input-->
<!-- type="file"-->
<!-- accept=".zip"-->
<!-- bind:this={fileInput}-->
<!-- class="hidden"-->
<!-- on:change={handleFileChange}-->
<!-- />-->
<!--{/snippet}-->

<div class="w-full flex">
{#each elements as menu (menu.name)}
Expand All @@ -109,3 +123,5 @@
</MenuButton>
{/each}
</div>

<ExportDialog bind:open={exportOpen} loading={exportLoading} archiveId={exportArchiveId} />
79 changes: 79 additions & 0 deletions src/lib/components/Menu/export-dialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '$lib/components/ui/dialog';
import { Spinner } from '$lib/components/ui/spinner';

interface Props {
open?: boolean;
loading?: boolean;
archiveId?: string | null;
}

let { open = $bindable(false), loading = false, archiveId = null }: Props = $props();

const EXPIRY_MS = 15 * 60 * 1000;

let expired = $state(false);

const downloadHref = $derived(archiveId && !expired ? `/fs/archive/${archiveId}` : null);

$effect(() => {
if (!archiveId) return;

expired = false;
const timer = setTimeout(() => (expired = true), EXPIRY_MS);
return () => clearTimeout(timer);
});
</script>

<Dialog bind:open>
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>Export project</DialogTitle>
<DialogDescription>
{#if loading}
Building the project archive…
{:else if expired}
This archive link has expired.
{:else}
Your archive is ready to download.
{/if}
</DialogDescription>
</DialogHeader>

<div class="flex flex-col items-center justify-center gap-3 py-8">
{#if loading}
<Spinner class="size-8 text-muted-foreground" />
<p class="text-sm text-muted-foreground">Preparing your archive…</p>
{:else if expired}
<span class="i-solar-close-circle-bold-duotone size-10 text-red-400"></span>
<p class="text-sm text-muted-foreground text-center">
This link is no longer available. Please export the project again.
</p>
{:else if downloadHref}
<span class="i-solar-check-circle-bold-duotone size-10 text-green-500"></span>
<a href={downloadHref} download>
<Button>
<span class="i-ic-baseline-download size-4"></span>
Download archive
</Button>
</a>
<p class="flex items-center gap-1.5 text-xs text-muted-foreground">
<span class="i-solar-clock-circle-bold-duotone size-3.5"></span>
This link expires after 15 minutes.
</p>
{/if}
</div>

<DialogFooter>
<Button variant="ghost" onclick={() => (open = false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
10 changes: 10 additions & 0 deletions src/lib/server/actions/project/archive/create.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useActionHandler } from '@utils-server/request-handler';

export interface CreateArchiveResult {
id: string;
}

export const createArchiveAction = useActionHandler(async ({ archive }) => {
const id = await archive.create();
return { id };
});
Loading
Loading