@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 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 require a Zod 4 schema. The adapter checks
24
- initial, loaded, externally changed, and outgoing values at the persistence
25
- boundary; it does not write browser storage.
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,7 @@
1
+ import { type Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ children: Snippet;
4
+ };
5
+ declare const ExportTarget: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ExportTarget = ReturnType<typeof ExportTarget>;
7
+ export default ExportTarget;
@@ -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}
@@ -0,0 +1,7 @@
1
+ import { type Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ children: Snippet;
4
+ };
5
+ declare const IconTarget: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type IconTarget = ReturnType<typeof IconTarget>;
7
+ export default IconTarget;
@@ -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
- choose(): void;
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 { slop } from "@hitslop/runtime";
2
- import { chooseLocalFile, fileToBase64, mediaSourceURL, safeMediaName } from "@hitslop/runtime/adapter";
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
- this.name = name;
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
- choose(): void;
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 { slop } from "@hitslop/runtime";
2
- import { chooseLocalFile, fileToBase64, mediaSourceURL, safeMediaName } from "@hitslop/runtime/adapter";
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
- this.name = name;
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 * as z from "zod";
2
- export type JsonStoreOptions<Schema extends z.ZodType> = {
3
- schema: Schema;
4
- initial: z.input<Schema>;
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
- type JsonSchema<T> = {
7
- safeParse(value: unknown): {
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: JsonSchema<T>;
16
+ readonly schema: S;
28
17
  private unwatch;
29
18
  private stopEffect;
30
- constructor(options: {
31
- schema: JsonSchema<T>;
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 const jsonStore: <Schema extends z.ZodType>(options: JsonStoreOptions<Schema>) => JsonStore<z.output<Schema>>;
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 json = JSON.stringify($state.snapshot(value));
6
- if (json === undefined)
7
- throw new Error("JsonStore values must be JSON-serializable");
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
- const result = this.schema.safeParse(value);
82
- if (result.success)
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 const jsonStore = (options) => new JsonStore(options);
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
+ }
@@ -6,6 +6,7 @@ export declare class SqliteQuery<T> {
6
6
  error: string | null;
7
7
  lastChangeSource: string;
8
8
  private unwatch;
9
+ private task;
9
10
  constructor(statement: SlopStatement);
10
11
  execute(statement: SlopStatement): Promise<void>;
11
12
  reload(): Promise<void>;
@@ -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() { this.isLoading = true; try {
16
- this.current = await slop.db.query(this.statement.sql, this.statement.parameters ?? []);
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
- catch (error) {
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.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Svelte 5 state adapters for the hitSlop runtime.",
5
5
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/hitslop/hitslop.git", "directory": "packages/svelte" },
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": { "url": "https://github.com/hitslop/hitslop/issues" },
9
- "keywords": ["hitslop", "svelte", "local-first", "mini-apps"],
10
- "publishConfig": { "access": "public" },
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
- ".": "./dist/index.js"
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 && tsc -p tsconfig.build.json",
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.1.2"
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
- "@typescript/native": "npm:typescript@^7.0.2",
34
- "typescript": "^6.0.3",
35
- "zod": "^4.5.2"
53
+ "typescript": "^6.0.3"
36
54
  }
37
55
  }