rune-lab 0.5.0-rc.2 → 0.5.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.
@@ -1,7 +1,16 @@
1
+ import type { Schema } from "effect";
1
2
  import type { ContributionEntry } from "./define-contribution.js";
2
3
  import type { SettingsSchema } from "./define-settings.js";
3
4
  import type { BaseSlotSpec } from "./define-slot.js";
4
5
  import { type SlotDescriptor } from "./descriptors.js";
6
+ export type SlotConfigInput<TSpec extends BaseSlotSpec> = TSpec extends {
7
+ config: Schema.Schema<infer TConfig, unknown, never>;
8
+ }
9
+ ? TConfig
10
+ : unknown;
11
+ export type PluginConfigInput<TSlots extends Record<string, BaseSlotSpec>> = {
12
+ [K in keyof TSlots]?: SlotConfigInput<TSlots[K]>;
13
+ };
5
14
  export interface ForgedPlugin<
6
15
  TId extends string = string,
7
16
  TSlots extends Record<string, BaseSlotSpec> = Record<string, BaseSlotSpec>,
@@ -13,6 +22,8 @@ export interface ForgedPlugin<
13
22
  overlays?: unknown[];
14
23
  contributions?: ContributionEntry<unknown>[];
15
24
  descriptors: Record<keyof TSlots, SlotDescriptor>;
25
+ config?: Record<string, unknown>;
26
+ with(config: PluginConfigInput<TSlots>): ForgedPlugin<TId, TSlots>;
16
27
  }
17
28
  export type PluginInput =
18
29
  | ForgedPlugin<string, Record<string, BaseSlotSpec>>
@@ -9,7 +9,7 @@ export function definePlugin(spec) {
9
9
  accessorName: getAccessorName(slotName),
10
10
  };
11
11
  }
12
- return {
12
+ const plugin = {
13
13
  id: spec.id,
14
14
  requires: spec.requires,
15
15
  slots: slots,
@@ -17,5 +17,15 @@ export function definePlugin(spec) {
17
17
  overlays: spec.overlays,
18
18
  contributions: spec.contributions,
19
19
  descriptors: descriptors,
20
+ with(config) {
21
+ return {
22
+ ...this,
23
+ config: {
24
+ ...(this.config || {}),
25
+ ...config,
26
+ },
27
+ };
28
+ },
20
29
  };
30
+ return plugin;
21
31
  }
@@ -13,6 +13,7 @@ export interface NormalizedSlot {
13
13
  expose: boolean;
14
14
  persist?: boolean | string[];
15
15
  configSchema?: unknown;
16
+ pluginConfig?: Record<string, unknown>;
16
17
  create: (context: SlotContext<unknown>) => unknown;
17
18
  }
18
19
  export declare function resolveSlotRef(
@@ -33,12 +33,6 @@ function normalizePlugins(inputs) {
33
33
  return true;
34
34
  });
35
35
  }
36
- function getPluginConfig(pluginId, config) {
37
- if (config[pluginId] !== undefined) {
38
- return config[pluginId];
39
- }
40
- return config;
41
- }
42
36
  function sortPlugins(resolved) {
43
37
  const pluginMap = new Map(resolved.map((p) => [p.id, p]));
44
38
  const graphNodes = resolved.map((p) => ({
@@ -97,6 +91,7 @@ export function normalizeSlots(sorted) {
97
91
  expose: slotSpec.expose !== false,
98
92
  persist: slotSpec.persist,
99
93
  configSchema: slotSpec.config,
94
+ pluginConfig: plugin.config,
100
95
  create: slotSpec.create,
101
96
  };
102
97
  allSlots.push(slot);
@@ -151,18 +146,22 @@ function buildLayers(slots, options) {
151
146
  const storeLayer = Layer.effect(
152
147
  storeTag,
153
148
  Effect.gen(function* () {
154
- const pluginConfig = getPluginConfig(slot.pluginId, options.config);
155
- let configSlice = pluginConfig;
149
+ const rawConfig = slot.pluginConfig?.[slot.slotName];
150
+ let configSlice = rawConfig;
156
151
  if (slot.configSchema) {
157
- try {
158
- configSlice = Schema.decodeUnknownSync(slot.configSchema)(
159
- pluginConfig,
160
- );
161
- } catch (err) {
162
- const errMsg = err instanceof Error ? err.message : String(err);
163
- throw new Error(
164
- `[Kernel] Config validation failed for plugin "${slot.pluginId}", slot "${slot.slotName}": ${errMsg}`,
165
- );
152
+ if (rawConfig !== undefined) {
153
+ try {
154
+ configSlice = Schema.decodeUnknownSync(slot.configSchema)(
155
+ rawConfig,
156
+ );
157
+ } catch (err) {
158
+ const errMsg = err instanceof Error ? err.message : String(err);
159
+ throw new Error(
160
+ `[Kernel] Config validation failed for plugin "${slot.pluginId}", slot "${slot.slotName}": ${errMsg}`,
161
+ );
162
+ }
163
+ } else {
164
+ configSlice = undefined;
166
165
  }
167
166
  }
168
167
  let persistenceHandle;
@@ -51,13 +51,21 @@ export function createThemeStore(ctx) {
51
51
  icon: "🎨",
52
52
  driver: ctx.persistence,
53
53
  });
54
- if (!store.current && BROWSER) {
55
- const prefersDark = globalThis.matchMedia(
56
- "(prefers-color-scheme: dark)",
57
- ).matches;
58
- const systemDefault = prefersDark ? "dark" : "light";
59
- if (store.get(systemDefault)) {
60
- store.set(systemDefault);
54
+ const saved = ctx.persistence.get("theme");
55
+ const hasPersisted =
56
+ typeof saved === "string" ? Boolean(saved && store.get(saved)) : false;
57
+ if (!hasPersisted) {
58
+ const configTheme = typeof ctx.config === "string" ? ctx.config : undefined;
59
+ if (configTheme && store.get(configTheme)) {
60
+ store.set(configTheme);
61
+ } else if (BROWSER) {
62
+ const prefersDark = globalThis.matchMedia(
63
+ "(prefers-color-scheme: dark)",
64
+ ).matches;
65
+ const systemDefault = prefersDark ? "dark" : "light";
66
+ if (store.get(systemDefault)) {
67
+ store.set(systemDefault);
68
+ }
61
69
  }
62
70
  }
63
71
  return store;
@@ -166,6 +166,8 @@ function formatKeys(keys: string) {
166
166
  <span><kbd class="kbd kbd-xs">ctrl</kbd> +
167
167
  <kbd class="kbd kbd-xs">/</kbd> to open help</span>
168
168
  </div>
169
- <div>rune-lab v{appStore.version}</div>
169
+ {#if appStore.data.version}
170
+ <div>rune-lab v{appStore.data.version}</div>
171
+ {/if}
170
172
  </div>
171
173
  </div>
@@ -1,14 +1,19 @@
1
1
  import type { ForgedPlugin, SlotSpec } from "rune-lab/core";
2
2
  import type { CommandStore } from "./commands/store.svelte.js";
3
3
  import type { ToastStore } from "./notifications/store.svelte.js";
4
- import type { PaletteRegistryStore } from "./registry/registry.svelte.js";
4
+ import type {
5
+ PaletteRegistryStore,
6
+ RouterAdapter,
7
+ } from "./registry/registry.svelte.js";
5
8
  import type { ShortcutStore } from "./shortcuts/store.svelte.js";
6
-
9
+ export interface RegistrySlotConfig {
10
+ router?: RouterAdapter;
11
+ }
7
12
  type PalettesSlots = {
8
13
  commands: SlotSpec<unknown, CommandStore>;
9
14
  shortcuts: SlotSpec<unknown, ShortcutStore>;
10
15
  toasts: SlotSpec<unknown, ToastStore>;
11
- registry: SlotSpec<unknown, PaletteRegistryStore>;
16
+ registry: SlotSpec<RegistrySlotConfig, PaletteRegistryStore>;
12
17
  };
13
18
  export declare const PalettesPlugin: ForgedPlugin<
14
19
  "rune-lab.palettes",
@@ -31,8 +31,8 @@ export const PalettesPlugin = definePlugin({
31
31
  expose: true,
32
32
  }),
33
33
  registry: defineSlot({
34
- create: () => {
35
- const store = createPaletteRegistryStore();
34
+ create: (ctx) => {
35
+ const store = createPaletteRegistryStore(ctx.config);
36
36
  store.register({
37
37
  id: "commands",
38
38
  title: "Commands",
@@ -1,4 +1,8 @@
1
1
  import type { Component } from "svelte";
2
+ export interface RouterAdapter {
3
+ replaceState?: (url: string) => void;
4
+ pushState?: (url: string) => void;
5
+ }
2
6
  export interface PaletteDefinition {
3
7
  id: string;
4
8
  title: string;
@@ -12,10 +16,16 @@ export declare class PaletteRegistryStore {
12
16
  palettes: PaletteDefinition[];
13
17
  activePaletteId: string | null;
14
18
  activeSectionId: string;
19
+ router?: RouterAdapter;
20
+ constructor(config?: {
21
+ router?: RouterAdapter;
22
+ });
15
23
  register(palette: PaletteDefinition): void;
16
24
  unregister(id: string): void;
17
25
  open(id: string, section?: string): void;
18
26
  setSection(sectionId: string): void;
19
27
  close(): void;
20
28
  }
21
- export declare function createPaletteRegistryStore(): PaletteRegistryStore;
29
+ export declare function createPaletteRegistryStore(config?: {
30
+ router?: RouterAdapter;
31
+ }): PaletteRegistryStore;
@@ -2,6 +2,10 @@ export class PaletteRegistryStore {
2
2
  palettes = $state([]);
3
3
  activePaletteId = $state(null);
4
4
  activeSectionId = $state("general");
5
+ router;
6
+ constructor(config) {
7
+ this.router = config?.router;
8
+ }
5
9
  register(palette) {
6
10
  if (this.palettes.some((p) => p.id === palette.id)) {
7
11
  return;
@@ -27,6 +31,6 @@ export class PaletteRegistryStore {
27
31
  this.activePaletteId = null;
28
32
  }
29
33
  }
30
- export function createPaletteRegistryStore() {
31
- return new PaletteRegistryStore();
34
+ export function createPaletteRegistryStore(config) {
35
+ return new PaletteRegistryStore(config);
32
36
  }
@@ -3,18 +3,18 @@ import { getAppStore } from "rune-lab";
3
3
 
4
4
  const appStore = getAppStore();
5
5
  const initial = $derived(
6
- appStore.name ? appStore.name.charAt(0).toUpperCase() : "",
6
+ appStore.data.name ? appStore.data.name.charAt(0).toUpperCase() : "",
7
7
  );
8
8
  </script>
9
9
 
10
10
  <div class="p-4 flex items-center gap-4 bg-base-200 rounded-box">
11
- {#if appStore.icon}
11
+ {#if appStore.data.icon}
12
12
  <img
13
- src={appStore.icon}
14
- alt={appStore.name}
13
+ src={appStore.data.icon}
14
+ alt={appStore.data.name ?? ""}
15
15
  class="w-12 h-12 rounded-box object-cover shrink-0"
16
16
  />
17
- {:else}
17
+ {:else if initial}
18
18
  <div class="flex items-center justify-center w-12 h-12 bg-primary/10 text-primary font-bold rounded-box text-xl shrink-0">
19
19
  {initial}
20
20
  </div>
@@ -22,29 +22,37 @@ const initial = $derived(
22
22
 
23
23
  <div class="flex-1 min-w-0">
24
24
  <div class="flex items-center gap-2">
25
- <span class="font-medium text-sm">{appStore.name}</span>
26
- <span class="badge badge-primary badge-sm">v{appStore.version}</span>
25
+ {#if appStore.data.name}
26
+ <span class="font-medium text-sm">{appStore.data.name}</span>
27
+ {/if}
28
+ {#if appStore.data.version}
29
+ <span class="badge badge-primary badge-sm">v{appStore.data.version}</span>
30
+ {/if}
27
31
  </div>
28
- <p class="text-base-content/60 text-xs truncate mt-0.5">{appStore.description}</p>
32
+ {#if appStore.data.description}
33
+ <p class="text-base-content/60 text-xs truncate mt-0.5">{appStore.data.description}</p>
34
+ {/if}
29
35
  </div>
30
36
 
31
37
  <div class="text-right shrink-0 flex flex-col items-end gap-1">
32
- <span class="text-base-content/70 text-xs font-medium">{appStore.author}</span>
38
+ {#if appStore.data.author}
39
+ <span class="text-base-content/70 text-xs font-medium">{appStore.data.author}</span>
40
+ {/if}
33
41
  <div class="flex items-center gap-2 text-xs text-base-content/50">
34
- {#if appStore.repository}
35
- <a href={appStore.repository} target="_blank" rel="noopener noreferrer" class="link link-hover text-xs">Repo</a>
42
+ {#if appStore.data.repository}
43
+ <a href={appStore.data.repository} target="_blank" rel="noopener noreferrer" class="link link-hover text-xs">Repo</a>
36
44
  {/if}
37
- {#if appStore.homepage}
38
- {#if appStore.repository}
45
+ {#if appStore.data.homepage}
46
+ {#if appStore.data.repository}
39
47
  <span>•</span>
40
48
  {/if}
41
- <a href={appStore.homepage} target="_blank" rel="noopener noreferrer" class="link link-hover text-xs">Home</a>
49
+ <a href={appStore.data.homepage} target="_blank" rel="noopener noreferrer" class="link link-hover text-xs">Home</a>
42
50
  {/if}
43
- {#if appStore.license}
44
- {#if appStore.repository || appStore.homepage}
51
+ {#if appStore.data.license}
52
+ {#if appStore.data.repository || appStore.data.homepage}
45
53
  <span>•</span>
46
54
  {/if}
47
- <span class="text-xs">{appStore.license}</span>
55
+ <span class="text-xs">{appStore.data.license}</span>
48
56
  {/if}
49
57
  </div>
50
58
  </div>
@@ -26,15 +26,21 @@ export function installSettingsRoute(registry) {
26
26
  if (activeId === "settings") {
27
27
  const want = formatSettingsHash(activeSec);
28
28
  if (globalThis.location.hash !== want) {
29
- history.replaceState(null, "", want);
29
+ if (registry.router?.replaceState) {
30
+ registry.router.replaceState(want);
31
+ } else {
32
+ history.replaceState(null, "", want);
33
+ }
30
34
  }
31
35
  } else {
32
36
  if (globalThis.location.hash.startsWith("#settings")) {
33
- history.pushState(
34
- null,
35
- "",
36
- globalThis.location.pathname + globalThis.location.search,
37
- );
37
+ const targetUrl =
38
+ globalThis.location.pathname + globalThis.location.search;
39
+ if (registry.router?.pushState) {
40
+ registry.router.pushState(targetUrl);
41
+ } else {
42
+ history.pushState(null, "", targetUrl);
43
+ }
38
44
  }
39
45
  }
40
46
  });
@@ -22,13 +22,10 @@ import { type AppData, createAppStore } from "./reactivity/app.svelte.js";
22
22
  export interface RuneLabConfig {
23
23
  persistence?: PersistenceDriver;
24
24
  /** Optional head management properties */
25
- favicon?: string;
26
25
  manageHead?: boolean;
27
26
  icons?: "material" | "none";
28
27
  /** App metadata — passed to AppStore.init() */
29
28
  app?: Partial<AppData>;
30
- /** Namespaced config for plugins */
31
- [pluginId: string]: unknown;
32
29
  }
33
30
 
34
31
  let {
@@ -59,11 +56,9 @@ const initialPersistence = untrack(() => {
59
56
  // 0. Create and provide the built-in AppStore
60
57
  const appStore = createAppStore();
61
58
  untrack(() => {
62
- const appData = {
63
- ...config.app,
64
- icon: config.app?.icon ?? config.favicon,
65
- };
66
- appStore.init(appData);
59
+ if (config.app) {
60
+ appStore.init(config.app);
61
+ }
67
62
  });
68
63
  setContext(RUNE_LAB_CONTEXT.app, appStore);
69
64
 
@@ -100,19 +95,23 @@ const allOverlays = $derived(kernel.overlays as Component[]);
100
95
 
101
96
  // Meta tags derived from app store state
102
97
  const metaTags = $derived([
103
- { name: "description", content: appStore.description },
104
- { name: "author", content: appStore.author },
98
+ { name: "description", content: appStore.data.description },
99
+ { name: "author", content: appStore.data.author },
105
100
  ]);
106
101
  </script>
107
102
 
108
103
  <svelte:head>
109
104
  {#if config.manageHead !== false && appStore}
110
- <title>{appStore.name}</title>
111
- {#if config.favicon}
112
- <link rel="icon" href={config.favicon} />
105
+ {#if appStore.data.name}
106
+ <title>{appStore.data.name}</title>
107
+ {/if}
108
+ {#if appStore.data.icon}
109
+ <link rel="icon" href={appStore.data.icon} />
113
110
  {/if}
114
111
  {#each metaTags as meta}
115
- <meta name={meta.name} content={meta.content} />
112
+ {#if meta.content}
113
+ <meta name={meta.name} content={meta.content} />
114
+ {/if}
116
115
  {/each}
117
116
  {#if config.icons === "material"}
118
117
  <link
@@ -12,13 +12,10 @@ import { type AppData } from "./reactivity/app.svelte.js";
12
12
  export interface RuneLabConfig {
13
13
  persistence?: PersistenceDriver;
14
14
  /** Optional head management properties */
15
- favicon?: string;
16
15
  manageHead?: boolean;
17
16
  icons?: "material" | "none";
18
17
  /** App metadata — passed to AppStore.init() */
19
18
  app?: Partial<AppData>;
20
- /** Namespaced config for plugins */
21
- [pluginId: string]: unknown;
22
19
  }
23
20
  type $$ComponentProps = {
24
21
  children: Snippet;
@@ -15,4 +15,4 @@ export { createAppStore, getAppStore } from "./reactivity/app.svelte.js";
15
15
  // Reactivity & Context exports
16
16
  export { useCell } from "./reactivity/use-cell.svelte.js";
17
17
  export { default as SettingsFields } from "./settings/SettingsFields.svelte";
18
- export const version = () => "0.5.0-rc.2";
18
+ export const version = () => "0.5.0";
@@ -1,3 +1,4 @@
1
+ import { type DataStore } from "./data-store.svelte.js";
1
2
  export interface AppData {
2
3
  name: string;
3
4
  version: string;
@@ -8,17 +9,6 @@ export interface AppData {
8
9
  homepage?: string;
9
10
  icon?: string;
10
11
  }
11
- export declare class AppStore {
12
- #private;
13
- name: string;
14
- version: string;
15
- description: string;
16
- author: string;
17
- repository: string;
18
- license: string;
19
- homepage: string;
20
- icon: string;
21
- init(data: Partial<AppData>): void;
22
- }
12
+ export type AppStore = DataStore<AppData>;
23
13
  export declare function createAppStore(): AppStore;
24
14
  export declare function getAppStore(): AppStore;
@@ -1,56 +1,19 @@
1
- import { DEV } from "esm-env";
2
1
  import { getContext } from "svelte";
3
2
  import { RUNE_LAB_CONTEXT } from "../provider/context.js";
4
- export class AppStore {
5
- name = $state("Rune Lab");
6
- version = $state("0.0.1");
7
- description = $state("Modern toolkit for Svelte 5 Runes");
8
- author = $state("Yrrrrrf");
9
- repository = $state("https://github.com/Yrrrrrf/rune-lab");
10
- license = $state("MIT");
11
- homepage = $state("https://jsr.io/@yrrrrrf/rune-lab");
12
- icon = $state("");
13
- #initialized = false;
14
- init(data) {
15
- if (this.#initialized) {
16
- if (DEV) {
17
- console.warn(
18
- "AppStore.init() called multiple times. Ignoring subsequent calls.",
19
- "Overwritten properties would have been:",
20
- data,
21
- );
22
- }
23
- return;
24
- }
25
- if (data.name) {
26
- this.name = data.name;
27
- }
28
- if (data.version) {
29
- this.version = data.version;
30
- }
31
- if (data.description) {
32
- this.description = data.description;
33
- }
34
- if (data.author) {
35
- this.author = data.author;
36
- }
37
- if (data.repository) {
38
- this.repository = data.repository;
39
- }
40
- if (data.license) {
41
- this.license = data.license;
42
- }
43
- if (data.homepage) {
44
- this.homepage = data.homepage;
45
- }
46
- if (data.icon) {
47
- this.icon = data.icon;
48
- }
49
- this.#initialized = true;
50
- }
51
- }
3
+ import { createDataStore } from "./data-store.svelte.js";
4
+
5
+ const APP_DATA_KEYS = [
6
+ "name",
7
+ "version",
8
+ "description",
9
+ "author",
10
+ "repository",
11
+ "license",
12
+ "homepage",
13
+ "icon",
14
+ ];
52
15
  export function createAppStore() {
53
- return new AppStore();
16
+ return createDataStore(APP_DATA_KEYS);
54
17
  }
55
18
  export function getAppStore() {
56
19
  const store = getContext(RUNE_LAB_CONTEXT.app);
@@ -0,0 +1,10 @@
1
+ export type NullableData<T> = {
2
+ [K in keyof T]: T[K] | null;
3
+ };
4
+ export interface DataStore<T extends object> {
5
+ readonly data: NullableData<T>;
6
+ init(input?: Partial<T>): void;
7
+ }
8
+ export declare function createDataStore<T extends object>(
9
+ keys: readonly (keyof T & string)[],
10
+ ): DataStore<T>;
@@ -0,0 +1,35 @@
1
+ import { DEV } from "esm-env";
2
+ export function createDataStore(keys) {
3
+ const initial = {};
4
+ for (const k of keys) {
5
+ initial[k] = null;
6
+ }
7
+ const data = $state(initial);
8
+ let initialized = false;
9
+ return {
10
+ get data() {
11
+ return data;
12
+ },
13
+ init(input) {
14
+ if (initialized) {
15
+ if (DEV) {
16
+ console.warn(
17
+ "DataStore.init() called multiple times. Ignoring subsequent calls.",
18
+ "Overwritten properties would have been:",
19
+ input,
20
+ );
21
+ }
22
+ return;
23
+ }
24
+ initialized = true;
25
+ if (input) {
26
+ for (const k of keys) {
27
+ const val = input[k];
28
+ if (val !== undefined && val !== null) {
29
+ data[k] = val;
30
+ }
31
+ }
32
+ }
33
+ },
34
+ };
35
+ }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { getContext } from "svelte";
2
+ import { getKernel } from "../provider/context.js";
3
3
  import { useCell } from "../reactivity/use-cell.svelte.js";
4
4
 
5
5
  let {
@@ -12,13 +12,15 @@ let {
12
12
  onCommit?: (fieldId: string, value: any) => void;
13
13
  } = $props();
14
14
 
15
+ const kernel = getKernel();
16
+
15
17
  // Resolve cells reactively using Svelte 5's $derived.by
16
18
  const cellBinds = $derived.by(() => {
17
19
  const binds = new Map<string, any>();
18
20
  for (const field of fields) {
19
21
  if (field.target?.type === "cell") {
20
22
  try {
21
- binds.set(field.id, useCell(field.target.name));
23
+ binds.set(field.id, useCell(kernel, field.target.name));
22
24
  } catch (e) {
23
25
  console.warn(
24
26
  `[SettingsFields] Failed to bind cell ${field.target.name}:`,
@@ -33,7 +35,8 @@ const cellBinds = $derived.by(() => {
33
35
  function getStoreForField(field: any): any {
34
36
  if (field.target?.type !== "store") return undefined;
35
37
  const pluginId = field.id.slice(0, field.id.lastIndexOf("."));
36
- return getContext(Symbol.for(`rl:${pluginId}:${field.target.storeId}`));
38
+ const storeKey = `rl:${pluginId}:${field.target.storeId}`;
39
+ return kernel.stores.get(storeKey);
37
40
  }
38
41
 
39
42
  function getValue(field: any): any {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune-lab",
3
- "version": "0.5.0-rc.2",
3
+ "version": "0.5.0",
4
4
  "description": "A comprehensive full-stack application template with Tauri integration",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",