@hitslop/svelte 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -4
- package/dist/ExportTarget.svelte +22 -0
- package/dist/ExportTarget.svelte.d.ts +7 -0
- package/dist/IconTarget.svelte +24 -0
- package/dist/IconTarget.svelte.d.ts +7 -0
- package/dist/file-store.svelte.d.ts +3 -14
- package/dist/file-store.svelte.js +4 -72
- package/dist/image-store.svelte.d.ts +3 -14
- package/dist/image-store.svelte.js +4 -75
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/json-store.svelte.d.ts +14 -27
- package/dist/json-store.svelte.js +27 -13
- package/dist/media-store.svelte.d.ts +25 -0
- package/dist/media-store.svelte.js +80 -0
- package/dist/sqlite-query.svelte.d.ts +1 -0
- package/dist/sqlite-query.svelte.js +6 -10
- package/package.json +31 -13
package/README.md
CHANGED
|
@@ -1,29 +1,101 @@
|
|
|
1
1
|
# @hitslop/svelte
|
|
2
2
|
|
|
3
|
+
Packaging uses this workspace's TypeScript 6 compiler API; checks use TypeScript
|
|
4
|
+
7 through `@typescript/native`, as does the root toolchain. The Bun patch for
|
|
5
|
+
`@sveltejs/package` resolves TypeScript from the project being packaged instead
|
|
6
|
+
of the packager's install directory. Keep the packager version pinned while the
|
|
7
|
+
patch is needed, and recheck this resolution when upgrading it.
|
|
8
|
+
|
|
3
9
|
Svelte 5 state adapters for JSON, SQLite, images, and named files in a hitSlop
|
|
4
10
|
document.
|
|
5
11
|
|
|
6
12
|
```svelte
|
|
7
13
|
<script lang="ts">
|
|
8
14
|
import { jsonStore } from "@hitslop/svelte";
|
|
15
|
+
import { ready } from "@hitslop/runtime";
|
|
16
|
+
import { onDestroy } from "svelte";
|
|
9
17
|
import counterSchema from "../schema";
|
|
10
18
|
|
|
11
19
|
const document = jsonStore({
|
|
12
20
|
schema: counterSchema,
|
|
13
21
|
initial: { count: 0 },
|
|
14
22
|
});
|
|
23
|
+
$effect(() => { if (document.isReady) ready(); });
|
|
24
|
+
onDestroy(() => document.destroy());
|
|
15
25
|
</script>
|
|
16
26
|
|
|
17
|
-
<button onclick={() => document.current.count += 1}>
|
|
27
|
+
<button disabled={!document.isReady || document.isLoading} onclick={() => document.current.count += 1}>
|
|
18
28
|
{document.current.count}
|
|
19
29
|
</button>
|
|
20
30
|
```
|
|
21
31
|
|
|
22
32
|
Exports: `jsonStore`, `sqliteQuery`, `imageStore`, `fileStore`, and their
|
|
23
|
-
class/type counterparts. JSON stores
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
class/type counterparts. JSON stores accept the default export of root TypeBox
|
|
34
|
+
`schema.ts` directly, with data types inferred from that schema. No preparation
|
|
35
|
+
or generated files are needed for editor types or typechecking. The adapter
|
|
36
|
+
validates initial, loaded, externally changed, and outgoing values using the
|
|
37
|
+
TypeBox interpreter; it does not coerce, insert defaults, strip unknown fields,
|
|
38
|
+
or write browser storage.
|
|
26
39
|
|
|
27
40
|
See the [Svelte authoring examples](https://github.com/hitslop/hitslop/tree/main/examples/slops).
|
|
28
41
|
|
|
29
42
|
MIT © 2026 hitSlop contributors.
|
|
43
|
+
|
|
44
|
+
## Capture views
|
|
45
|
+
|
|
46
|
+
Svelte is the supported authoring integration. Keep the interactive editor in
|
|
47
|
+
`App.svelte`; optionally provide `Icon.svelte` and `Export.svelte` as ordinary
|
|
48
|
+
presentation components. They share data and styles with the editor, not another
|
|
49
|
+
store or persistence model:
|
|
50
|
+
|
|
51
|
+
```svelte
|
|
52
|
+
<script lang="ts">
|
|
53
|
+
import { IconTarget, ExportTarget } from "@hitslop/svelte";
|
|
54
|
+
import Icon from "./Icon.svelte";
|
|
55
|
+
import Export from "./Export.svelte";
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<IconTarget><Icon completed={finished} total={tasks.length} /></IconTarget>
|
|
59
|
+
<ExportTarget><Export checklist={checklist.current} view={activeView} /></ExportTarget>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`IconTarget` mounts its children only in icon capture. It supplies a transparent
|
|
63
|
+
512×512 surface; supplying it opts into Finder icon refresh when a document
|
|
64
|
+
closes. The signed `QuickLook/Icon.png` is immutable; only Finder metadata changes.
|
|
65
|
+
Without an icon target, the existing static icon remains.
|
|
66
|
+
|
|
67
|
+
`ExportTarget` mounts its children for previews and PNG/PDF exports. Pass the
|
|
68
|
+
current selected view explicitly. Keep content in normal flow, with no fixed
|
|
69
|
+
viewport heights, nested scrolling, editing controls, or transient notices.
|
|
70
|
+
Share presentation components and theme variables to avoid visual drift.
|
|
71
|
+
`Export.svelte` is optional: without a target, the existing app is captured with
|
|
72
|
+
`data-slop-capture="static"` and `data-slop-export="hide"` controls omitted.
|
|
73
|
+
|
|
74
|
+
Preview stays at the manifest viewport. Export uses the current window width
|
|
75
|
+
and full content height. Dedicated exports have their own rectangular content
|
|
76
|
+
surface rather than a stretched window mask. PNG is 2×, limited to 16,384 pixels
|
|
77
|
+
per side and 24 megapixels; use PDF for longer documents. PDF is one page sized
|
|
78
|
+
to the content, with selectable text. Very long PDFs combine WebKit's pages and
|
|
79
|
+
scale uniformly to a maximum 14,400-point page dimension, preserving all content
|
|
80
|
+
and vector sharpness within common PDF reader limits. Their physical page width
|
|
81
|
+
is scaled too; no content is clipped or converted to a bitmap.
|
|
82
|
+
|
|
83
|
+
The runtime waits for target mounting, used fonts, visible image decoding, and
|
|
84
|
+
stable geometry, with a ten-second timeout for each preparation/settling stage.
|
|
85
|
+
Motion is disabled during capture. Canvas, charts, CSS background images, or
|
|
86
|
+
virtualized lists can register extra work with
|
|
87
|
+
`capture.onPrepare(async (mode, signal) => { ... })`; await required assets or
|
|
88
|
+
rendering and respect the abort signal. The returned function unregisters the
|
|
89
|
+
hook. Hooks should not modify durable data. Do not call `ready()` until initial
|
|
90
|
+
data is usable.
|
|
91
|
+
|
|
92
|
+
Use `?capture=icon` or `?capture=export` in `slop dev` or the shared gallery to
|
|
93
|
+
inspect disposable capture views. Reload to return to normal editing. Native
|
|
94
|
+
capture remains the authority for image/PDF fidelity.
|
|
95
|
+
|
|
96
|
+
Background captures operate on temporary snapshots, including a SQLite backup,
|
|
97
|
+
and cannot initialize or modify the source stores. User exports use the current
|
|
98
|
+
session to preserve view state; captures are serialized and restore the editor
|
|
99
|
+
on success or failure. Preview and icon failures are independent. Close-time
|
|
100
|
+
refreshes keep the last successful images on failure; quit gives pending jobs
|
|
101
|
+
up to five seconds after data has been saved.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { capture } from "@hitslop/runtime";
|
|
3
|
+
import { onMount, tick, type Snippet } from "svelte";
|
|
4
|
+
let { children }: { children: Snippet } = $props();
|
|
5
|
+
let visible = $state(false);
|
|
6
|
+
let element = $state<HTMLDivElement>();
|
|
7
|
+
onMount(() => {
|
|
8
|
+
|
|
9
|
+
const node = element;
|
|
10
|
+
if (!node) return;
|
|
11
|
+
document.body.append(node);
|
|
12
|
+
const unregister = capture.registerTarget("export", {
|
|
13
|
+
element: node,
|
|
14
|
+
prepare: async () => { visible = true; await tick(); },
|
|
15
|
+
restore: async () => { visible = false; await tick(); },
|
|
16
|
+
});
|
|
17
|
+
return () => { unregister(); node.remove(); };
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
<div bind:this={element} data-slop-render="export" style:display={visible ? "block" : "none"} style:width="100%">
|
|
21
|
+
{#if visible}{@render children()}{/if}
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { capture } from "@hitslop/runtime";
|
|
3
|
+
import { onMount, tick, type Snippet } from "svelte";
|
|
4
|
+
let { children }: { children: Snippet } = $props();
|
|
5
|
+
let visible = $state(false);
|
|
6
|
+
let element = $state<HTMLDivElement>();
|
|
7
|
+
onMount(() => {
|
|
8
|
+
if (!capture.isRenderer()) return;
|
|
9
|
+
const node = element;
|
|
10
|
+
if (!node) return;
|
|
11
|
+
document.body.append(node);
|
|
12
|
+
const unregister = capture.registerTarget("icon", {
|
|
13
|
+
element: node,
|
|
14
|
+
prepare: async () => { visible = true; await tick(); },
|
|
15
|
+
restore: async () => { visible = false; await tick(); },
|
|
16
|
+
});
|
|
17
|
+
return () => { unregister(); node.remove(); };
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
{#if capture.isRenderer()}
|
|
21
|
+
<div bind:this={element} data-slop-render="icon" style:display={visible ? "block" : "none"} style:width="512px" style:height="512px" style:background="transparent">
|
|
22
|
+
{#if visible}{@render children()}{/if}
|
|
23
|
+
</div>
|
|
24
|
+
{/if}
|
|
@@ -1,21 +1,10 @@
|
|
|
1
|
+
import { MediaStore } from "./media-store.svelte.js";
|
|
1
2
|
export type FileStoreOptions = {
|
|
2
3
|
accept?: string;
|
|
3
4
|
};
|
|
4
|
-
export declare class FileStore {
|
|
5
|
-
readonly name: string;
|
|
5
|
+
export declare class FileStore extends MediaStore<null> {
|
|
6
6
|
readonly options: FileStoreOptions;
|
|
7
|
-
src: string | null;
|
|
8
|
-
hasCustomFile: boolean;
|
|
9
|
-
isLoading: boolean;
|
|
10
|
-
error: string | null;
|
|
11
|
-
revision: string | null;
|
|
12
|
-
private unwatch;
|
|
13
7
|
constructor(name: string, options?: FileStoreOptions);
|
|
14
|
-
|
|
15
|
-
replace(file: File): Promise<void>;
|
|
16
|
-
remove(): Promise<void>;
|
|
17
|
-
reload(): Promise<void>;
|
|
18
|
-
destroy(): void;
|
|
19
|
-
private adopt;
|
|
8
|
+
get hasCustomFile(): boolean;
|
|
20
9
|
}
|
|
21
10
|
export declare const fileStore: (name: string, options?: FileStoreOptions) => FileStore;
|
|
@@ -1,78 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export class FileStore {
|
|
4
|
-
name;
|
|
1
|
+
import { MediaStore } from "./media-store.svelte.js";
|
|
2
|
+
export class FileStore extends MediaStore {
|
|
5
3
|
options;
|
|
6
|
-
src = $state(null);
|
|
7
|
-
hasCustomFile = $state(false);
|
|
8
|
-
isLoading = $state(true);
|
|
9
|
-
error = $state(null);
|
|
10
|
-
revision = $state(null);
|
|
11
|
-
unwatch = null;
|
|
12
4
|
constructor(name, options = {}) {
|
|
13
|
-
|
|
5
|
+
super(name, null, options.accept ?? "");
|
|
14
6
|
this.options = options;
|
|
15
|
-
safeMediaName(name, "File");
|
|
16
|
-
void this.reload();
|
|
17
|
-
this.unwatch = slop.media.onChange(() => { void this.reload(); });
|
|
18
|
-
}
|
|
19
|
-
choose() {
|
|
20
|
-
chooseLocalFile(this.options.accept ?? "", (file) => { void this.replace(file).catch(() => undefined); });
|
|
21
|
-
}
|
|
22
|
-
async replace(file) {
|
|
23
|
-
this.isLoading = true;
|
|
24
|
-
this.error = null;
|
|
25
|
-
try {
|
|
26
|
-
const data = await fileToBase64(file);
|
|
27
|
-
const result = await slop.media.write(this.name, data, file.type || "application/octet-stream");
|
|
28
|
-
this.adopt(true, result.revision);
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
this.isLoading = false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async remove() {
|
|
39
|
-
this.isLoading = true;
|
|
40
|
-
this.error = null;
|
|
41
|
-
try {
|
|
42
|
-
await slop.media.remove(this.name);
|
|
43
|
-
this.adopt(false, null);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
this.isLoading = false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async reload() {
|
|
54
|
-
this.isLoading = true;
|
|
55
|
-
try {
|
|
56
|
-
const result = await slop.media.open(this.name);
|
|
57
|
-
this.adopt(result.exists, result.revision);
|
|
58
|
-
this.error = null;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
62
|
-
this.adopt(false, null);
|
|
63
|
-
}
|
|
64
|
-
finally {
|
|
65
|
-
this.isLoading = false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
destroy() {
|
|
69
|
-
this.unwatch?.();
|
|
70
|
-
this.unwatch = null;
|
|
71
|
-
}
|
|
72
|
-
adopt(exists, revision) {
|
|
73
|
-
this.hasCustomFile = exists;
|
|
74
|
-
this.revision = revision;
|
|
75
|
-
this.src = exists ? mediaSourceURL(this.name, revision) : null;
|
|
76
7
|
}
|
|
8
|
+
get hasCustomFile() { return this.hasCustomMedia; }
|
|
77
9
|
}
|
|
78
10
|
export const fileStore = (name, options = {}) => new FileStore(name, options);
|
|
@@ -1,21 +1,10 @@
|
|
|
1
|
+
import { MediaStore } from "./media-store.svelte.js";
|
|
1
2
|
export type ImageStoreOptions = {
|
|
2
3
|
fallback: string;
|
|
3
4
|
};
|
|
4
|
-
export declare class ImageStore {
|
|
5
|
-
readonly name: string;
|
|
5
|
+
export declare class ImageStore extends MediaStore<string> {
|
|
6
6
|
readonly options: ImageStoreOptions;
|
|
7
|
-
src: string;
|
|
8
|
-
hasCustomImage: boolean;
|
|
9
|
-
isLoading: boolean;
|
|
10
|
-
error: string | null;
|
|
11
|
-
revision: string | null;
|
|
12
|
-
private unwatch;
|
|
13
7
|
constructor(name: string, options: ImageStoreOptions);
|
|
14
|
-
|
|
15
|
-
replace(file: File): Promise<void>;
|
|
16
|
-
remove(): Promise<void>;
|
|
17
|
-
reload(): Promise<void>;
|
|
18
|
-
destroy(): void;
|
|
19
|
-
private adopt;
|
|
8
|
+
get hasCustomImage(): boolean;
|
|
20
9
|
}
|
|
21
10
|
export declare const imageStore: (name: string, options: ImageStoreOptions) => ImageStore;
|
|
@@ -1,81 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export class ImageStore {
|
|
4
|
-
name;
|
|
1
|
+
import { MediaStore } from "./media-store.svelte.js";
|
|
2
|
+
export class ImageStore extends MediaStore {
|
|
5
3
|
options;
|
|
6
|
-
src = $state("");
|
|
7
|
-
hasCustomImage = $state(false);
|
|
8
|
-
isLoading = $state(true);
|
|
9
|
-
error = $state(null);
|
|
10
|
-
revision = $state(null);
|
|
11
|
-
unwatch = null;
|
|
12
4
|
constructor(name, options) {
|
|
13
|
-
|
|
5
|
+
super(name, options.fallback, "image/*");
|
|
14
6
|
this.options = options;
|
|
15
|
-
safeMediaName(name, "Image");
|
|
16
|
-
this.src = options.fallback;
|
|
17
|
-
void this.reload();
|
|
18
|
-
this.unwatch = slop.media.onChange(() => { void this.reload(); });
|
|
19
|
-
}
|
|
20
|
-
choose() {
|
|
21
|
-
chooseLocalFile("image/*", (file) => { void this.replace(file); });
|
|
22
|
-
}
|
|
23
|
-
async replace(file) {
|
|
24
|
-
if (!file.type.startsWith("image/")) {
|
|
25
|
-
this.error = "Choose an image file.";
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
this.isLoading = true;
|
|
29
|
-
this.error = null;
|
|
30
|
-
try {
|
|
31
|
-
const data = await fileToBase64(file, "image");
|
|
32
|
-
const result = await slop.media.write(this.name, data, file.type);
|
|
33
|
-
this.adopt(true, result.revision);
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
37
|
-
}
|
|
38
|
-
finally {
|
|
39
|
-
this.isLoading = false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
async remove() {
|
|
43
|
-
this.isLoading = true;
|
|
44
|
-
this.error = null;
|
|
45
|
-
try {
|
|
46
|
-
await slop.media.remove(this.name);
|
|
47
|
-
this.adopt(false, null);
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
this.isLoading = false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async reload() {
|
|
57
|
-
this.isLoading = true;
|
|
58
|
-
try {
|
|
59
|
-
const result = await slop.media.open(this.name);
|
|
60
|
-
this.adopt(result.exists, result.revision);
|
|
61
|
-
this.error = null;
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
65
|
-
this.adopt(false, null);
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
this.isLoading = false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
destroy() {
|
|
72
|
-
this.unwatch?.();
|
|
73
|
-
this.unwatch = null;
|
|
74
|
-
}
|
|
75
|
-
adopt(exists, revision) {
|
|
76
|
-
this.hasCustomImage = exists;
|
|
77
|
-
this.revision = revision;
|
|
78
|
-
this.src = exists ? mediaSourceURL(this.name, revision) : this.options.fallback;
|
|
79
7
|
}
|
|
8
|
+
get hasCustomImage() { return this.hasCustomMedia; }
|
|
80
9
|
}
|
|
81
10
|
export const imageStore = (name, options) => new ImageStore(name, options);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { jsonStore, JsonStore, type JsonStoreOptions } from "./json-store.svelte
|
|
|
2
2
|
export { imageStore, ImageStore, type ImageStoreOptions } from "./image-store.svelte.js";
|
|
3
3
|
export { fileStore, FileStore, type FileStoreOptions } from "./file-store.svelte.js";
|
|
4
4
|
export { sqliteQuery, SqliteQuery } from "./sqlite-query.svelte.js";
|
|
5
|
+
export { default as IconTarget } from "./IconTarget.svelte";
|
|
6
|
+
export { default as ExportTarget } from "./ExportTarget.svelte";
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,5 @@ export { jsonStore, JsonStore } from "./json-store.svelte.js";
|
|
|
2
2
|
export { imageStore, ImageStore } from "./image-store.svelte.js";
|
|
3
3
|
export { fileStore, FileStore } from "./file-store.svelte.js";
|
|
4
4
|
export { sqliteQuery, SqliteQuery } from "./sqlite-query.svelte.js";
|
|
5
|
+
export { default as IconTarget } from "./IconTarget.svelte";
|
|
6
|
+
export { default as ExportTarget } from "./ExportTarget.svelte";
|
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export type JsonStoreOptions<
|
|
3
|
-
schema:
|
|
4
|
-
initial:
|
|
1
|
+
import type { Static, TSchema } from "typebox";
|
|
2
|
+
export type JsonStoreOptions<S extends TSchema> = {
|
|
3
|
+
schema: S;
|
|
4
|
+
initial: NoInfer<Static<S>>;
|
|
5
5
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
success: true;
|
|
9
|
-
data: T;
|
|
10
|
-
} | {
|
|
11
|
-
success: false;
|
|
12
|
-
error: {
|
|
13
|
-
issues: ReadonlyArray<{
|
|
14
|
-
path: PropertyKey[];
|
|
15
|
-
message: string;
|
|
16
|
-
}>;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
export declare class JsonStore<T> {
|
|
21
|
-
current: T;
|
|
6
|
+
export declare class JsonStore<S extends TSchema> {
|
|
7
|
+
current: Static<S>;
|
|
22
8
|
isLoading: boolean;
|
|
9
|
+
isReady: boolean;
|
|
10
|
+
isDirty: boolean;
|
|
11
|
+
isSaving: boolean;
|
|
23
12
|
error: string | null;
|
|
24
13
|
revision: string | null;
|
|
25
14
|
lastChangeSource: string;
|
|
26
15
|
private persister;
|
|
27
|
-
readonly schema:
|
|
16
|
+
readonly schema: S;
|
|
28
17
|
private unwatch;
|
|
29
18
|
private stopEffect;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
initial: unknown;
|
|
33
|
-
});
|
|
19
|
+
private unregisterFlush;
|
|
20
|
+
constructor(options: JsonStoreOptions<S>);
|
|
34
21
|
reload(): Promise<void>;
|
|
35
22
|
destroy(): void;
|
|
23
|
+
flush(): Promise<void>;
|
|
36
24
|
private parse;
|
|
37
25
|
}
|
|
38
|
-
export declare
|
|
39
|
-
export {};
|
|
26
|
+
export declare function jsonStore<S extends TSchema>(options: JsonStoreOptions<S>): JsonStore<S>;
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { slop } from "@hitslop/runtime";
|
|
2
|
-
import { JsonPersister } from "@hitslop/runtime/adapter";
|
|
2
|
+
import { JsonPersister, registerFlush } from "@hitslop/runtime/adapter";
|
|
3
3
|
import { untrack } from "svelte";
|
|
4
|
+
import { validate } from "@hitslop/schema/validation";
|
|
5
|
+
import { assertJSON } from "@hitslop/schema/json";
|
|
4
6
|
const snapshot = (value) => {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const detached = $state.snapshot(value);
|
|
8
|
+
assertJSON(detached);
|
|
9
|
+
const json = JSON.stringify(detached);
|
|
8
10
|
return { json, value: JSON.parse(json) };
|
|
9
11
|
};
|
|
10
12
|
export class JsonStore {
|
|
11
13
|
current = $state();
|
|
12
14
|
isLoading = $state(true);
|
|
15
|
+
isReady = $state(false);
|
|
16
|
+
isDirty = $state(false);
|
|
17
|
+
isSaving = $state(false);
|
|
13
18
|
error = $state(null);
|
|
14
19
|
revision = $state(null);
|
|
15
20
|
lastChangeSource = $state("package");
|
|
@@ -17,6 +22,7 @@ export class JsonStore {
|
|
|
17
22
|
schema;
|
|
18
23
|
unwatch = null;
|
|
19
24
|
stopEffect = null;
|
|
25
|
+
unregisterFlush = null;
|
|
20
26
|
constructor(options) {
|
|
21
27
|
this.schema = options.schema;
|
|
22
28
|
const fallbackSnapshot = snapshot(this.parse(options.initial));
|
|
@@ -39,6 +45,7 @@ export class JsonStore {
|
|
|
39
45
|
onRevision: (revision) => { this.revision = revision; },
|
|
40
46
|
onSource: (source) => { this.lastChangeSource = source; },
|
|
41
47
|
onError: (message) => { this.error = message; },
|
|
48
|
+
onStatus: ({ isDirty, isSaving }) => { this.isDirty = isDirty; this.isSaving = isSaving; },
|
|
42
49
|
});
|
|
43
50
|
// Snapshotting reads the complete proxy tree, so one effect run observes all
|
|
44
51
|
// nested mutations. This intentionally favors small, document-sized JSON.
|
|
@@ -57,12 +64,14 @@ export class JsonStore {
|
|
|
57
64
|
});
|
|
58
65
|
});
|
|
59
66
|
void this.reload();
|
|
67
|
+
this.unregisterFlush = registerFlush(() => this.flush());
|
|
60
68
|
this.unwatch = slop.json.onChange((event) => this.persister.externalChanged(event.revision));
|
|
61
69
|
}
|
|
62
70
|
async reload() {
|
|
63
71
|
this.isLoading = true;
|
|
64
72
|
try {
|
|
65
73
|
await this.persister.reload();
|
|
74
|
+
this.isReady = true;
|
|
66
75
|
}
|
|
67
76
|
catch (error) {
|
|
68
77
|
this.error = error instanceof Error ? error.message : String(error);
|
|
@@ -72,20 +81,25 @@ export class JsonStore {
|
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
destroy() {
|
|
84
|
+
if (!this.isLoading)
|
|
85
|
+
void this.flush().catch(() => undefined);
|
|
86
|
+
this.unregisterFlush?.();
|
|
87
|
+
this.unregisterFlush = null;
|
|
75
88
|
this.unwatch?.();
|
|
76
89
|
this.unwatch = null;
|
|
77
90
|
this.stopEffect?.();
|
|
78
91
|
this.stopEffect = null;
|
|
79
92
|
}
|
|
93
|
+
async flush() {
|
|
94
|
+
const local = snapshot(this.parse(this.current));
|
|
95
|
+
this.persister.localChanged(local.json, local.value);
|
|
96
|
+
await this.persister.flush();
|
|
97
|
+
}
|
|
80
98
|
parse(value) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return result.data;
|
|
84
|
-
const detail = result.error.issues.map((issue) => {
|
|
85
|
-
const path = issue.path.length ? issue.path.join(".") : "value";
|
|
86
|
-
return `${path}: ${issue.message}`;
|
|
87
|
-
}).join("; ");
|
|
88
|
-
throw new Error(`JSON schema validation failed: ${detail}`);
|
|
99
|
+
assertJSON(value);
|
|
100
|
+
return validate(this.schema, value);
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
|
-
export
|
|
103
|
+
export function jsonStore(options) {
|
|
104
|
+
return new JsonStore(options);
|
|
105
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Shared lifecycle for named files and images; subclasses only supply policy. */
|
|
2
|
+
export declare class MediaStore<Fallback extends string | null> {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
private readonly fallback;
|
|
5
|
+
private readonly accept;
|
|
6
|
+
src: string | Fallback | undefined;
|
|
7
|
+
hasCustomMedia: boolean;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
revision: string | null;
|
|
11
|
+
private readonly task;
|
|
12
|
+
private readonly unwatch;
|
|
13
|
+
private mutations;
|
|
14
|
+
private disposed;
|
|
15
|
+
private failure;
|
|
16
|
+
private readonly unregisterFlush;
|
|
17
|
+
constructor(name: string, fallback: Fallback, accept: string);
|
|
18
|
+
choose(): void;
|
|
19
|
+
replace(file: File): Promise<void>;
|
|
20
|
+
remove(): Promise<void>;
|
|
21
|
+
private mutate;
|
|
22
|
+
flush(): Promise<void>;
|
|
23
|
+
reload(): Promise<void>;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { slop } from "@hitslop/runtime";
|
|
2
|
+
import { chooseLocalFile, fileToBase64, LatestTask, mediaSourceURL, registerFlush, safeMediaName } from "@hitslop/runtime/adapter";
|
|
3
|
+
/** Shared lifecycle for named files and images; subclasses only supply policy. */
|
|
4
|
+
export class MediaStore {
|
|
5
|
+
name;
|
|
6
|
+
fallback;
|
|
7
|
+
accept;
|
|
8
|
+
src = $state();
|
|
9
|
+
hasCustomMedia = $state(false);
|
|
10
|
+
isLoading = $state(true);
|
|
11
|
+
error = $state(null);
|
|
12
|
+
revision = $state(null);
|
|
13
|
+
task = new LatestTask();
|
|
14
|
+
unwatch;
|
|
15
|
+
mutations = Promise.resolve();
|
|
16
|
+
disposed = false;
|
|
17
|
+
failure;
|
|
18
|
+
unregisterFlush;
|
|
19
|
+
constructor(name, fallback, accept) {
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.fallback = fallback;
|
|
22
|
+
this.accept = accept;
|
|
23
|
+
safeMediaName(name);
|
|
24
|
+
this.src = fallback;
|
|
25
|
+
this.unregisterFlush = registerFlush(() => this.flush());
|
|
26
|
+
this.unwatch = slop.media.onChange((event) => { if (!event.name || event.name === name)
|
|
27
|
+
void this.reload(); });
|
|
28
|
+
void this.reload();
|
|
29
|
+
}
|
|
30
|
+
choose() { chooseLocalFile(this.accept, (file) => { void this.replace(file).catch(() => undefined); }); }
|
|
31
|
+
async replace(file) {
|
|
32
|
+
if (this.accept === "image/*" && !file.type.startsWith("image/")) {
|
|
33
|
+
this.error = "Choose an image file.";
|
|
34
|
+
throw new Error(this.error);
|
|
35
|
+
}
|
|
36
|
+
await this.mutate(async () => {
|
|
37
|
+
const data = await fileToBase64(file);
|
|
38
|
+
const result = await slop.media.write(this.name, data, file.type || "application/octet-stream");
|
|
39
|
+
return { exists: true, revision: result.revision };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async remove() {
|
|
43
|
+
await this.mutate(async () => { await slop.media.remove(this.name); return { exists: false, revision: null }; });
|
|
44
|
+
}
|
|
45
|
+
async mutate(operation) {
|
|
46
|
+
if (this.disposed)
|
|
47
|
+
throw new Error("Media store is closed.");
|
|
48
|
+
this.task.invalidate();
|
|
49
|
+
this.isLoading = true;
|
|
50
|
+
this.error = null;
|
|
51
|
+
const mutation = this.mutations.then(async () => { await operation(); this.failure = undefined; });
|
|
52
|
+
this.mutations = mutation.catch(() => undefined);
|
|
53
|
+
try {
|
|
54
|
+
await mutation;
|
|
55
|
+
await this.reload();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.failure = error;
|
|
59
|
+
if (!this.disposed) {
|
|
60
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
61
|
+
this.isLoading = false;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async flush() { await this.mutations; if (this.failure)
|
|
67
|
+
throw this.failure; }
|
|
68
|
+
async reload() {
|
|
69
|
+
if (this.disposed)
|
|
70
|
+
return;
|
|
71
|
+
this.isLoading = true;
|
|
72
|
+
await this.task.run(() => slop.media.open(this.name), (result) => {
|
|
73
|
+
this.hasCustomMedia = result.exists;
|
|
74
|
+
this.revision = result.revision;
|
|
75
|
+
this.src = result.exists ? mediaSourceURL(this.name, result.revision) : this.fallback;
|
|
76
|
+
this.error = null;
|
|
77
|
+
}, (error) => { this.error = error instanceof Error ? error.message : String(error); }, () => { this.isLoading = false; });
|
|
78
|
+
}
|
|
79
|
+
destroy() { this.disposed = true; this.task.dispose(); this.unwatch(); this.unregisterFlush(); }
|
|
80
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { slop } from "@hitslop/runtime";
|
|
2
|
+
import { LatestTask } from "@hitslop/runtime/adapter";
|
|
2
3
|
export class SqliteQuery {
|
|
3
4
|
statement;
|
|
4
5
|
current = $state([]);
|
|
@@ -6,22 +7,17 @@ export class SqliteQuery {
|
|
|
6
7
|
error = $state(null);
|
|
7
8
|
lastChangeSource = $state("package");
|
|
8
9
|
unwatch = null;
|
|
10
|
+
task = new LatestTask();
|
|
9
11
|
constructor(statement) {
|
|
10
12
|
this.statement = statement;
|
|
11
13
|
void this.reload();
|
|
12
14
|
this.unwatch = slop.db.onChange((event) => { this.lastChangeSource = event.source; void this.reload(); });
|
|
13
15
|
}
|
|
14
16
|
async execute(statement) { await slop.db.execute(statement.sql, statement.parameters ?? []); }
|
|
15
|
-
async reload() {
|
|
16
|
-
this.
|
|
17
|
-
this.error = null;
|
|
17
|
+
async reload() {
|
|
18
|
+
this.isLoading = true;
|
|
19
|
+
await this.task.run(() => slop.db.query(this.statement.sql, this.statement.parameters ?? []), (rows) => { this.current = rows; this.error = null; }, (error) => { this.error = error instanceof Error ? error.message : String(error); }, () => { this.isLoading = false; });
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
this.error = error instanceof Error ? error.message : String(error);
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
this.isLoading = false;
|
|
24
|
-
} }
|
|
25
|
-
destroy() { this.unwatch?.(); this.unwatch = null; }
|
|
21
|
+
destroy() { this.task.dispose(); this.unwatch?.(); this.unwatch = null; }
|
|
26
22
|
}
|
|
27
23
|
export const sqliteQuery = (statement) => new SqliteQuery(statement);
|
package/package.json
CHANGED
|
@@ -1,37 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitslop/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Svelte 5 state adapters for the hitSlop runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/hitslop/hitslop.git",
|
|
9
|
+
"directory": "packages/svelte"
|
|
10
|
+
},
|
|
7
11
|
"homepage": "https://hitslop.com",
|
|
8
|
-
"bugs": {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/hitslop/hitslop/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"hitslop",
|
|
17
|
+
"svelte",
|
|
18
|
+
"local-first",
|
|
19
|
+
"mini-apps"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
11
24
|
"type": "module",
|
|
12
25
|
"files": [
|
|
13
26
|
"dist"
|
|
14
27
|
],
|
|
15
28
|
"exports": {
|
|
16
|
-
".":
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"svelte": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
17
34
|
},
|
|
18
35
|
"scripts": {
|
|
19
|
-
"build": "bun ../../scripts/clean-dist.ts &&
|
|
36
|
+
"build": "bun ../../scripts/clean-dist.ts && svelte-package -i src -o dist",
|
|
20
37
|
"check": "svelte-check --tsgo --tsconfig ./tsconfig.json",
|
|
21
38
|
"test": "bun run --cwd ../runtime build && bun test"
|
|
22
39
|
},
|
|
23
40
|
"dependencies": {
|
|
24
|
-
"@hitslop/runtime": "^0.
|
|
41
|
+
"@hitslop/runtime": "^0.2.0",
|
|
42
|
+
"@hitslop/schema": "^0.2.0",
|
|
43
|
+
"typebox": "^1.3.26"
|
|
25
44
|
},
|
|
26
45
|
"peerDependencies": {
|
|
27
|
-
"svelte": "^5.0.0"
|
|
28
|
-
"zod": "^4.0.0"
|
|
46
|
+
"svelte": "^5.0.0"
|
|
29
47
|
},
|
|
30
48
|
"devDependencies": {
|
|
49
|
+
"@sveltejs/package": "2.5.8",
|
|
50
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
31
51
|
"svelte": "^5.57.0",
|
|
32
52
|
"svelte-check": "^4.7.6",
|
|
33
|
-
"
|
|
34
|
-
"typescript": "^6.0.3",
|
|
35
|
-
"zod": "^4.5.2"
|
|
53
|
+
"typescript": "^6.0.3"
|
|
36
54
|
}
|
|
37
55
|
}
|