commit c4494c3a98e606f12fc74f7a2bcace92c6e8d537
parent d2a56f1d37d8ae3f2e506eeffa94fc99aa91b5e9
Author: triesap <triesap@radroots.dev>
Date: Sat, 27 Dec 2025 15:06:43 +0000
app: harden startup flows and expand PWA caching
- Guard db/geocoder startup with idempotent promise tracking
- Add cache-first service worker with app-shell fallback and range support
- Refactor setup carousel/view stack state and tighten error handling
- Wire new workspace packages and env example files into tooling
Diffstat:
17 files changed, 963 insertions(+), 653 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -16,6 +16,8 @@ logs/
.env
.env.*
!.env.example
+!.env.development.example
+!.env.production.example
!.env.test
.local*
justfile
diff --git a/app/package.json b/app/package.json
@@ -41,6 +41,7 @@
"@radroots/http": "workspace:*",
"@radroots/geocoder": "workspace:*",
"@radroots/locales": "workspace:*",
+ "@radroots/nfc": "workspace:*",
"@radroots/nostr": "workspace:*",
"@radroots/tangle-schema-bindings": "workspace:*",
"@radroots/themes": "workspace:*",
diff --git a/app/src/app.css b/app/src/app.css
@@ -20,7 +20,7 @@
}
@source "./**/*.{svelte,ts}";
-@source "../../packages/lib-app/src/**/*.{svelte,ts}";
+@source "../../packages/apps-lib/src/**/*.{svelte,ts}";
@source "../../packages/apps-lib-pwa/src/**/*.{svelte,ts}";
@theme {
diff --git a/app/src/lib/utils/app/cipher.ts b/app/src/lib/utils/app/cipher.ts
@@ -1,4 +1,4 @@
-import { WebAesGcmCipher, type WebAesGcmCipherConfig } from "@radroots/client/keystore";
+import { WebAesGcmCipher, type WebAesGcmCipherConfig } from "@radroots/client/cipher";
import { cfg_data } from "../config";
const sql_cipher_config = (store_key: string): WebAesGcmCipherConfig => ({
@@ -8,5 +8,6 @@ const sql_cipher_config = (store_key: string): WebAesGcmCipherConfig => ({
export const reset_sql_cipher = async (store_key: string): Promise<void> => {
const cipher = new WebAesGcmCipher(sql_cipher_config(store_key));
- await cipher.reset();
-};
-\ No newline at end of file
+ const res = await cipher.reset();
+ if (res && "err" in res) throw new Error(res.err);
+};
diff --git a/app/src/lib/utils/app/handlers.ts b/app/src/lib/utils/app/handlers.ts
@@ -1,7 +1,7 @@
import { theme_mode, type LocalCallbackColorMode, type LocalCallbackGeocode, type LocalCallbackGeocodeCurrent, type LocalCallbackGuiAlert, type LocalCallbackGuiConfirm, type LocalCallbackImgBin, type LocalCallbackPhotosAddMultiple, type LocalCallbackPhotosUpload } from "@radroots/apps-lib";
import { parse_theme_mode } from "@radroots/themes";
import { throw_err } from "@radroots/utils";
-import { fs, geoc, geol, http, notif } from ".";
+import { fs, geoc, geoc_init, geol, http, notif } from ".";
type PhotoUploadResponse = {
res_base: string;
@@ -30,7 +30,7 @@ export const lc_gui_confirm: LocalCallbackGuiConfirm = async (opts) => {
};
export const lc_geocode: LocalCallbackGeocode = async (geoc_p) => {
- await geoc.connect();
+ await geoc_init();
const geoc_res = await geoc.reverse(geoc_p);
if ("err" in geoc_res) throw_err(geoc_res);
return geoc_res.results[0] || undefined;
diff --git a/app/src/lib/utils/app/index.ts b/app/src/lib/utils/app/index.ts
@@ -52,6 +52,36 @@ export const db = new WebTangleDatabase({
});
let db_i: Promise<WebTangleDatabase> | null = null;
+let db_init_promise: Promise<void> | null = null;
+let geoc_init_promise: Promise<void> | null = null;
+
+export const db_init = async (): Promise<void> => {
+ if (!db_init_promise) {
+ db_init_promise = (async () => {
+ await db.init();
+ })();
+ }
+ try {
+ await db_init_promise;
+ } catch (e) {
+ db_init_promise = null;
+ throw e;
+ }
+};
+
+export const geoc_init = async (): Promise<void> => {
+ if (!geoc_init_promise) {
+ geoc_init_promise = (async () => {
+ await geoc.connect();
+ })();
+ }
+ try {
+ await geoc_init_promise;
+ } catch (e) {
+ geoc_init_promise = null;
+ throw e;
+ }
+};
export const create_db = async (): Promise<WebTangleDatabase> => {
if (!db_i) {
diff --git a/app/src/lib/utils/backup/export.ts b/app/src/lib/utils/backup/export.ts
@@ -1,4 +1,4 @@
-import { datastore, db, nostr_keys, notif } from "$lib/utils/app";
+import { datastore, db, db_init, nostr_keys, notif } from "$lib/utils/app";
import { ls } from "$lib/utils/i18n";
import { download_json, get_store, handle_err } from "@radroots/apps-lib";
import type { ExportedAppState } from "@radroots/apps-lib-pwa/types/app";
@@ -53,7 +53,7 @@ const export_nostr_keystore_state = async (): Promise<ExportedAppState["nostr_ke
};
const export_tangle_db_state = async (): Promise<ExportedAppState["database"]> => {
- await db.init();
+ await db_init();
const store_key = db.get_store_key();
const backup = await db.export_backup();
if ("err" in backup) throw_err(backup);
diff --git a/app/src/lib/utils/backup/import.ts b/app/src/lib/utils/backup/import.ts
@@ -35,9 +35,9 @@ const assert_config_match = (
};
export const validate_import_file = async (file: File | null): Promise<ImportableAppState> => {
- const parsed: any = await parse_file_json(file)
- if (!parsed) throw_err(ls_val(`error.configuration.import.invalid_file_contents`))
- return await validate_import_state(parsed);
+ const parsed_res = await parse_file_json(file);
+ if (!parsed_res.ok) throw_err(ls_val(`error.configuration.import.invalid_file_contents`));
+ return await validate_import_state(parsed_res.value);
};
export const validate_import_state = async (state: any): Promise<ImportableAppState> => {
diff --git a/app/src/routes/(app)/+layout.svelte b/app/src/routes/(app)/+layout.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
- import { db, nostr_keys } from "$lib/utils/app";
+ import { db_init, nostr_keys } from "$lib/utils/app";
import { nostr_login_nip01 } from "@radroots/apps-nostr";
import { nostr_context_default, nostr_relays_clear, nostr_relays_open } from "@radroots/nostr";
import { handle_err, throw_err } from "@radroots/utils";
@@ -21,7 +21,7 @@
});
const init = async (): Promise<void> => {
- await db.init();
+ await db_init();
};
const nostr_init = async (): Promise<void> => {
diff --git a/app/src/routes/(app)/farms/+page.svelte b/app/src/routes/(app)/farms/+page.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
- import { db, route } from "$lib/utils/app";
+ import { db, db_init, route } from "$lib/utils/app";
import { handle_err } from "@radroots/apps-lib";
import { Farms } from "@radroots/apps-lib-pwa";
import type {
@@ -13,7 +13,7 @@
let data: LoadData = $state(undefined);
onMount(async () => {
- await db.init();
+ await db_init();
data = await load_data();
});
diff --git a/app/src/routes/(app)/profile/+page.svelte b/app/src/routes/(app)/profile/+page.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
- import { db, fs, nostr_keys, notif, radroots, route } from "$lib/utils/app";
+ import { db, db_init, fs, nostr_keys, notif, radroots, route } from "$lib/utils/app";
import { ls } from "$lib/utils/i18n";
import { parse_file_path } from "@radroots/apps-lib";
import { nostr_pubkey } from "@radroots/apps-nostr";
@@ -16,7 +16,7 @@
onMount(async () => {
try {
// await init();
- await db.init();
+ await db_init();
data = await load_data();
} catch (e) {
handle_err(e, `on_mount`);
@@ -25,7 +25,7 @@
});
const init = async (): Promise<void> => {
- await db.init();
+ await db_init();
};
const load_data = async (): Promise<IViewProfileData | undefined> => {
diff --git a/app/src/routes/(app)/profile/edit/+page.svelte b/app/src/routes/(app)/profile/edit/+page.svelte
@@ -4,7 +4,7 @@
import { ProfileEdit } from "@radroots/apps-lib-pwa";
import { qp_field, qp_keynostr } from "@radroots/apps-lib-pwa/stores/app";
import type { IViewProfileEditData } from "@radroots/apps-lib-pwa/types/views/profile";
- import { parse_view_profile_field_key } from "@radroots/apps-lib-pwa/utils/profile/lib";
+ import { parse_view_profile_field_key } from "@radroots/apps-lib-pwa/utils/profile";
import { handle_err, throw_err } from "@radroots/utils";
import { onMount } from "svelte";
diff --git a/app/src/routes/(cfg)/setup/+page.svelte b/app/src/routes/(cfg)/setup/+page.svelte
@@ -17,16 +17,17 @@
import { ls } from "$lib/utils/i18n";
import { get_default_nostr_relays } from "$lib/utils/nostr/lib";
import {
+ carousel_create,
carousel_dec,
carousel_inc,
- casl_i,
- casl_imax,
+ carousel_init,
el_id,
Fade,
fmt_id,
Glyph,
sleep,
- view_effect,
+ ViewPane,
+ ViewStack,
} from "@radroots/apps-lib";
import {
ButtonLayoutPair,
@@ -49,6 +50,8 @@
} from "@radroots/utils";
import { onMount } from "svelte";
+ type View = "cfg_key" | "cfg_profile" | "eula";
+
const page_carousel: Record<View, { max_index: number }> = {
cfg_key: {
max_index: 2,
@@ -61,16 +64,60 @@
},
};
- type View = "cfg_key" | "cfg_profile" | "eula";
- let view: View = $state("cfg_key");
- $effect(() => {
- view_effect<View>(view);
+ const carousel_cfg_key = carousel_create({
+ view: "cfg_key",
+ max_index: page_carousel.cfg_key.max_index,
});
+ const carousel_cfg_profile = carousel_create({
+ view: "cfg_profile",
+ max_index: page_carousel.cfg_profile.max_index,
+ });
+
+ const carousel_eula = carousel_create({
+ view: "eula",
+ max_index: page_carousel.eula.max_index,
+ });
+
+ const view_carousel = {
+ cfg_key: carousel_cfg_key,
+ cfg_profile: carousel_cfg_profile,
+ eula: carousel_eula,
+ };
+
+ const carousel_cfg_profile_index = carousel_cfg_profile.index;
+
+ let view: View = $state("cfg_key");
+
let cfg_role: AppConfigRole | undefined = $state(undefined);
type CfgKeyOpt = "nostr_key_gen" | "nostr_key_add";
let cgf_key_opt: CfgKeyOpt | undefined = $state(undefined);
+ type CfgKeyStep = "intro" | "choice" | "add_existing";
+ let cfg_key_step: CfgKeyStep = $state("intro");
+
+ const cfg_key_step_index = (step: CfgKeyStep): number => {
+ switch (step) {
+ case "intro":
+ return 0;
+ case "choice":
+ return 1;
+ case "add_existing":
+ return 2;
+ }
+ };
+
+ $effect(() => {
+ console.log(`view `, view);
+ console.log(`cfg_key_step `, cfg_key_step);
+ });
+
+ const cfg_key_step_for_index = (index: number): CfgKeyStep => {
+ if (index <= 0) return "intro";
+ if (index === 1) return "choice";
+ return "add_existing";
+ };
+
let nostr_key_add_val = $state(``);
let profile_name_val = $state(``);
@@ -81,6 +128,33 @@
let is_eula_scrolled = $state(false);
let is_loading_s = $state(false);
+ const reset_local_state = (): void => {
+ cfg_role = undefined;
+ cgf_key_opt = undefined;
+ cfg_key_step = "intro";
+ nostr_key_add_val = ``;
+ profile_name_val = ``;
+ profile_name_valid = false;
+ profile_name_nip05 = true;
+ profile_name_loading = false;
+ is_eula_scrolled = false;
+ is_loading_s = false;
+ };
+
+ const sync_carousel = (new_view: View, index: number): void => {
+ const carousel = view_carousel[new_view];
+ carousel_init(carousel, {
+ index,
+ max_index: page_carousel[new_view].max_index,
+ });
+ };
+
+ const set_cfg_key_step = (step: CfgKeyStep): void => {
+ cfg_key_step = step;
+ // @todo confirm why this was breaking the correct... if (step === "choice" && !cgf_key_opt) cgf_key_opt = "nostr_key_gen";
+ sync_carousel("cfg_key", cfg_key_step_index(step));
+ };
+
onMount(async () => {
try {
await page_init();
@@ -90,6 +164,7 @@
});
const page_init = async (): Promise<void> => {
+ reset_local_state();
const nostr_keys_all = await nostr_keys.keys();
if ("results" in nostr_keys_all) {
const confirm = await notif.confirm({
@@ -106,8 +181,9 @@
return;
}
await page_reset();
+ return;
}
- handle_view(view);
+ handle_view(`cfg_key`, { index: 0 });
};
const page_reset = async (
@@ -117,7 +193,8 @@
try {
console.log(`[config] page reset`);
app_loading.set(!prevent_loading);
- handle_view(`cfg_key`);
+ reset_local_state();
+ handle_view(`cfg_key`, { index: 0 });
if (alert_message) await notif.alert(alert_message);
await sleep(cfg_delay.load);
await nostr_keys.reset();
@@ -142,17 +219,26 @@
if (scroll_top + client_h >= scroll_h) is_eula_scrolled = true;
};
- const handle_view = (new_view: View): void => {
- if (new_view === "cfg_key" && view === "cfg_profile") {
- const offset = cgf_key_opt === "nostr_key_gen" ? 1 : 0;
- casl_i.set(page_carousel[new_view].max_index - offset);
- } else {
- casl_i.set(0);
- casl_imax.set(page_carousel[new_view].max_index);
+ const handle_view = (new_view: View, opts?: { index?: number }): void => {
+ let next_index = opts?.index ?? 0;
+ if (new_view === "cfg_key") {
+ if (opts?.index !== undefined)
+ cfg_key_step = cfg_key_step_for_index(opts.index);
+ else if (view === "cfg_profile")
+ cfg_key_step =
+ cgf_key_opt === "nostr_key_add" ? "add_existing" : "choice";
+ if (cfg_key_step === "choice" && !cgf_key_opt)
+ cgf_key_opt = "nostr_key_gen";
+ next_index = cfg_key_step_index(cfg_key_step);
}
view = new_view;
+ sync_carousel(new_view, next_index);
};
+ $effect(() => {
+ console.log(`cgf_key_opt `, cgf_key_opt);
+ });
+
const handle_config_err = async (
err?: IError<string> | string,
): Promise<void> => {
@@ -166,16 +252,16 @@
const handle_continue = async (): Promise<void> => {
switch (view) {
case `cfg_key`:
- switch ($casl_i) {
- case 0:
- return await carousel_inc(view);
- case 1:
+ switch (cfg_key_step) {
+ case "intro":
+ return set_cfg_key_step("choice");
+ case "choice":
return handle_new_key_or_add();
- case 2:
+ case "add_existing":
return handle_key_add_existing();
}
case `cfg_profile`:
- switch ($casl_i) {
+ switch ($carousel_cfg_profile_index) {
case 0:
return handle_setup_profile();
case 1:
@@ -187,29 +273,45 @@
const handle_new_key_or_add = async (): Promise<void> => {
try {
if (cgf_key_opt === `nostr_key_add`)
- return void (await carousel_inc(view));
- await create_nostr_key();
+ return set_cfg_key_step("add_existing");
+ const key_created = await create_nostr_key();
+ if (!key_created) return;
handle_view(`cfg_profile`);
} catch (e) {
handle_err(e, `handle_new_key_or_add`);
}
};
- const create_nostr_key = async (): Promise<void> => {
+ const create_nostr_key = async (): Promise<boolean> => {
const keys_nostr_gen = await nostr_keys.generate();
- if ("err" in keys_nostr_gen) return handle_config_err();
- await datastore.update_obj<ConfigData>("cfg_data", {
+ if ("err" in keys_nostr_gen) {
+ await handle_config_err(keys_nostr_gen);
+ return false;
+ }
+ const cfg_update = await datastore.update_obj<ConfigData>("cfg_data", {
nostr_public_key: keys_nostr_gen.public_key,
});
+ if ("err" in cfg_update) {
+ await handle_config_err(cfg_update);
+ return false;
+ }
+ return true;
};
- const add_nostr_key = async (secret_key: string): Promise<void> => {
+ const add_nostr_key = async (secret_key: string): Promise<boolean> => {
const keys_nostr_add = await nostr_keys.add(secret_key);
- if ("err" in keys_nostr_add)
- return void (await notif.alert(`${$ls(`common.invalid_key`)}`));
- await datastore.update_obj<ConfigData>("cfg_data", {
+ if ("err" in keys_nostr_add) {
+ await notif.alert(`${$ls(`common.invalid_key`)}`);
+ return false;
+ }
+ const cfg_update = await datastore.update_obj<ConfigData>("cfg_data", {
nostr_public_key: keys_nostr_add.public_key,
});
+ if ("err" in cfg_update) {
+ await handle_config_err(cfg_update);
+ return false;
+ }
+ return true;
};
const handle_key_add_existing = async (): Promise<void> => {
@@ -227,7 +329,8 @@
value: `${$ls(`common.nostr_key`)}`.toLowerCase(),
})}`,
));
- await add_nostr_key(secret_key);
+ const key_added = await add_nostr_key(secret_key);
+ if (!key_added) return;
nostr_key_add_val = ``;
handle_view(`cfg_profile`);
} catch (e) {
@@ -243,13 +346,15 @@
const handle_setup_profile = async (): Promise<void> => {
try {
if (profile_name_loading) return;
-
+ //@
const ds_cfg_data = await datastore.get_obj<ConfigData>("cfg_data");
+ console.log(`ds_cfg_data `, ds_cfg_data);
if ("err" in ds_cfg_data) return handle_config_err();
const ks_nostr_key = await nostr_keys.read(
ds_cfg_data.result.nostr_public_key,
);
+ console.log(`ks_nostr_key `, ks_nostr_key);
if ("err" in ks_nostr_key) return handle_config_err();
if (profile_name_nip05) {
@@ -307,7 +412,8 @@
}
if (!profile_name_val) {
- const confirm = handle_add_profile_without_name_confirmation();
+ const confirm =
+ await handle_add_profile_without_name_confirmation();
if (!confirm)
return void el_id(fmt_id(`nostr:profile`))?.focus();
}
@@ -318,7 +424,7 @@
});
}
- await carousel_inc(view);
+ await carousel_inc(view_carousel[view]);
} catch (e) {
handle_err(e, `handle_setup_profile`);
} finally {
@@ -346,18 +452,18 @@
const handle_back = async (): Promise<void> => {
switch (view) {
case `cfg_key`:
- switch ($casl_i) {
- case 1: {
+ switch (cfg_key_step) {
+ case "choice": {
cgf_key_opt = undefined;
- return await carousel_dec(view);
+ return set_cfg_key_step("intro");
}
- case 2: {
+ case "add_existing": {
nostr_key_add_val = ``;
- return await carousel_dec(view);
+ return set_cfg_key_step("choice");
}
}
case `cfg_profile`:
- switch ($casl_i) {
+ switch ($carousel_cfg_profile_index) {
case 0: {
if (!profile_name_val) {
const confirm =
@@ -366,12 +472,12 @@
return void el_id(
fmt_id(`nostr:profile`),
)?.focus();
- return void carousel_inc(view);
+ return void carousel_inc(view_carousel[view]);
}
return handle_view(`cfg_key`);
}
case 1:
- return carousel_dec(view);
+ return carousel_dec(view_carousel[view]);
}
}
};
@@ -458,9 +564,13 @@
await datastore.del_obj("cfg_data");
return { pass: true };
};
+
+ $effect(() => {
+ console.log(`view `, view);
+ });
</script>
-{#if view === "cfg_key" && $casl_i > 0}
+{#if view === "cfg_key" && cfg_key_step !== "intro"}
<Fade basis={{ classes: `z-10 absolute top-8 right-6` }}>
<SelectMenu
basis={{
@@ -496,629 +606,674 @@
</Fade>
{/if}
-<div
- data-view={`cfg_key`}
- class={`flex flex-col h-full w-full justify-start items-center`}
+<ViewStack
+ basis={{
+ active_view: view,
+ }}
>
- <CarouselContainer
- basis={{
- view: `cfg_key`,
- }}
- >
- <CarouselItem
- basis={{
- view: `cfg_key`,
- classes: `justify-center items-center`,
- }}
- >
- <div
- class={`relative flex flex-col h-full w-full justify-center items-center`}
+ <ViewPane basis={{ view: "cfg_key" }}>
+ <div class={`flex flex-col h-full w-full justify-start items-center`}>
+ <CarouselContainer
+ basis={{
+ carousel: carousel_cfg_key,
+ }}
>
- <div
- class={`flex flex-row w-full justify-start items-center -translate-y-16`}
- >
- <button
- class={`flex flex-row w-full justify-center items-center`}
- onclick={async () => {
- await goto(`/`);
- }}
- >
- <LogoCircle />
- </button>
- </div>
- <div
- class={`absolute bottom-0 left-0 flex flex-col h-[20rem] w-full px-10 gap-2 justify-start items-center`}
+ <CarouselItem
+ basis={{
+ classes: `justify-center items-center`,
+ }}
>
<div
- class={`flex flex-row w-full justify-start items-center`}
+ class={`relative flex flex-col h-full w-full justify-center items-center`}
>
- <p
- class={`font-sans font-[400] text-sm text-ly0-gl-label uppercase`}
+ <div
+ class={`flex flex-row w-full justify-start items-center -translate-y-16`}
>
- {`${$ls(`common.configure`)}`}
- </p>
+ <button
+ class={`flex flex-row w-full justify-center items-center`}
+ onclick={async () => {
+ await goto(`/`);
+ }}
+ >
+ <LogoCircle />
+ </button>
+ </div>
+ <div
+ class={`absolute bottom-0 left-0 flex flex-col h-[20rem] w-full px-10 gap-2 justify-start items-center`}
+ >
+ <div
+ class={`flex flex-row w-full justify-start items-center`}
+ >
+ <p
+ class={`font-sans font-[400] text-sm text-ly0-gl-label uppercase`}
+ >
+ {`${$ls(`common.configure`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-center`}
+ >
+ <div
+ class={`flex flex-row w-full justify-start items-center`}
+ >
+ <p
+ class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`}
+ >
+ {`${$ls(`notification.init.greeting_header`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-row w-full justify-start items-center`}
+ >
+ <p
+ class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`}
+ >
+ {`${$ls(
+ `notification.init.greeting_subheader`,
+ )}.`}
+ </p>
+ </div>
+ </div>
+ </div>
</div>
+ </CarouselItem>
+ <CarouselItem
+ basis={{
+ classes: `justify-center items-center`,
+ }}
+ >
<div
- class={`flex flex-col w-full gap-2 justify-start items-center`}
+ class={`flex flex-col h-[16rem] gap-8 w-full justify-start items-center`}
>
<div
- class={`flex flex-row w-full justify-start items-center`}
+ class={`flex flex-row w-full justify-center items-center`}
>
<p
- class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`}
+ class={`font-sans font-[600] text-ly0-gl text-3xl`}
>
- {`${$ls(`notification.init.greeting_header`)}`}
+ {`${$ls(`icu.configure_*`, {
+ value: `${$ls(`common.device`)}`,
+ })}`}
</p>
</div>
<div
- class={`flex flex-row w-full justify-start items-center`}
+ class={`flex flex-col w-full gap-6 justify-center items-center`}
>
- <p
- class={`font-mono font-[400] text-[1.1rem] text-ly0-gl`}
+ <button
+ class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
+ cgf_key_opt === `nostr_key_gen`
+ ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
+ : `bg-ly1`
+ } el-re`}
+ onclick={async (ev) => {
+ ev.stopPropagation();
+ cgf_key_opt = `nostr_key_gen`;
+ }}
>
- {`${$ls(
- `notification.init.greeting_subheader`,
- )}.`}
- </p>
+ <p
+ class={`font-sans font-[600] text-ly0-gl text-xl`}
+ >
+ {`${$ls(`icu.create_new_*`, {
+ value: `${$ls(
+ `common.keypair`,
+ )}`.toLowerCase(),
+ })}`}
+ </p>
+ </button>
+ <button
+ class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
+ cgf_key_opt === `nostr_key_add`
+ ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
+ : `bg-ly1`
+ } el-re`}
+ onclick={async (ev) => {
+ ev.stopPropagation();
+ cgf_key_opt = `nostr_key_add`;
+ }}
+ >
+ <p
+ class={`font-sans font-[600] text-ly0-gl text-xl`}
+ >
+ {`${$ls(`icu.use_existing_*`, {
+ value: `${$ls(
+ `common.keypair`,
+ )}`.toLowerCase(),
+ })}`}
+ </p>
+ </button>
</div>
</div>
- </div>
- </div>
- </CarouselItem>
- <CarouselItem
- basis={{
- view: `cfg_key`,
- classes: `justify-center items-center`,
- role: `button`,
- tabindex: 0,
- callback_click: async () => {
- cgf_key_opt = undefined;
- },
- }}
- >
- <div
- class={`flex flex-col h-[16rem] gap-8 w-full justify-start items-center`}
- >
- <div class={`flex flex-row w-full justify-center items-center`}>
- <p class={`font-sans font-[600] text-ly0-gl text-3xl`}>
- {`${$ls(`icu.configure_*`, {
- value: `${$ls(`common.device`)}`,
- })}`}
- </p>
- </div>
- <div
- class={`flex flex-col w-full gap-6 justify-center items-center`}
+ </CarouselItem>
+ <CarouselItem
+ basis={{
+ classes: `justify-center items-center`,
+ }}
>
- <button
- class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
- cgf_key_opt === `nostr_key_gen`
- ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
- : `bg-ly1`
- } el-re`}
- onclick={async (ev) => {
- ev.stopPropagation();
- cgf_key_opt = `nostr_key_gen`;
- }}
- >
- <p class={`font-sans font-[600] text-ly0-gl text-xl`}>
- {`${$ls(`icu.create_new_*`, {
- value: `${$ls(`common.keypair`)}`.toLowerCase(),
- })}`}
- </p>
- </button>
- <button
- class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
- cgf_key_opt === `nostr_key_add`
- ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
- : `bg-ly1`
- } el-re`}
- onclick={async (ev) => {
- ev.stopPropagation();
- cgf_key_opt = `nostr_key_add`;
- }}
+ <div
+ class={`flex flex-col w-full gap-8 justify-start items-center`}
>
- <p class={`font-sans font-[600] text-ly0-gl text-xl`}>
- {`${$ls(`icu.use_existing_*`, {
- value: `${$ls(`common.keypair`)}`.toLowerCase(),
- })}`}
- </p>
- </button>
- </div>
- </div>
- </CarouselItem>
- <CarouselItem
- basis={{
- view: `cfg_key`,
- classes: `justify-center items-center`,
- }}
- >
- <div
- class={`flex flex-col w-full gap-8 justify-start items-center`}
- >
+ <div
+ class={`flex flex-col w-full gap-6 justify-center items-center`}
+ >
+ <p
+ class={`font-sans font-[600] text-ly0-gl text-3xl capitalize`}
+ >
+ {`${$ls(`icu.add_existing_*`, {
+ value: `${$ls(`common.key`)}`.toLowerCase(),
+ })}`}
+ </p>
+ <EntryLine
+ bind:value={nostr_key_add_val}
+ basis={{
+ wrap: {
+ layer: 1,
+ classes: `w-lo_${$app_lo}`,
+ style: `guide`,
+ },
+ el: {
+ classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`,
+ layer: 1,
+ placeholder: `${$ls(`icu.enter_*`, {
+ value: `${$ls(
+ `common.nostr_nsec_hex`,
+ )}`,
+ })}`,
+ callback_keydown: async ({
+ key_s,
+ el,
+ }) => {
+ if (key_s) {
+ el.blur();
+ handle_continue();
+ }
+ },
+ },
+ }}
+ />
+ </div>
+ </div>
+ </CarouselItem>
<div
- class={`flex flex-col w-full gap-6 justify-center items-center`}
+ class={`z-10 absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`}
>
- <p
- class={`font-sans font-[600] text-ly0-gl text-3xl capitalize`}
- >
- {`${$ls(`icu.add_existing_*`, {
- value: `${$ls(`common.key`)}`.toLowerCase(),
- })}`}
- </p>
- <EntryLine
- bind:value={nostr_key_add_val}
+ <ButtonLayoutPair
basis={{
- wrap: {
- layer: 1,
- classes: `w-lo_${$app_lo}`,
- style: `guide`,
+ continue: {
+ label: `${$ls(`common.continue`)}`,
+ disabled:
+ cfg_key_step === "choice" && !cgf_key_opt,
+ callback: async () => handle_continue(),
},
- el: {
- classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`,
- layer: 1,
- placeholder: `${$ls(`icu.enter_*`, {
- value: `${$ls(`common.nostr_nsec_hex`)}`,
- })}`,
- callback_keydown: async ({ key_s, el }) => {
- if (key_s) {
- el.blur();
- handle_continue();
- }
- },
+ back: {
+ label: `${$ls(`common.back`)}`,
+ visible: cfg_key_step !== "intro",
+ callback: async () => handle_back(),
},
}}
/>
</div>
- </div>
- </CarouselItem>
- <div
- class={`z-10 absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`}
- >
- <ButtonLayoutPair
- basis={{
- continue: {
- label: `${$ls(`common.continue`)}`,
- disabled: $casl_i === 1 && !cgf_key_opt,
- callback: async () => handle_continue(),
- },
- back: {
- label: `${$ls(`common.back`)}`,
- visible: $casl_i > 0,
- callback: async () => handle_back(),
- },
- }}
- />
+ </CarouselContainer>
</div>
- </CarouselContainer>
-</div>
+ </ViewPane>
-<div
- data-view={`cfg_profile`}
- class={`hidden flex flex-col h-full w-full justify-start items-center`}
->
- <CarouselContainer
- basis={{
- view: `cfg_profile`,
- }}
- >
- <CarouselItem
- basis={{
- view: `cfg_profile`,
- classes: `justify-center items-center`,
- }}
- >
- <div
- class={`flex flex-col h-[16rem] w-full px-4 gap-6 justify-start items-center`}
+ <ViewPane basis={{ view: "cfg_profile" }}>
+ <div class={`flex flex-col h-full w-full justify-start items-center`}>
+ <CarouselContainer
+ basis={{
+ carousel: carousel_cfg_profile,
+ }}
>
- <p class={`font-sans font-[600] text-ly0-gl text-3xl`}>
- {`${$ls(`icu.add_*`, {
- value: `${$ls(`common.profile`)}`,
- })}`}
- </p>
- <div
- class={`flex flex-col w-full gap-4 justify-center items-center`}
+ <CarouselItem
+ basis={{
+ classes: `justify-center items-center`,
+ }}
>
- <EntryLine
- bind:value={profile_name_val}
- basis={{
- loading: profile_name_loading,
- wrap: {
- layer: 1,
- classes: `w-lo_${$app_lo}`,
- style: `guide`,
- },
- el: {
- classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`,
- id: fmt_id(`nostr:profile`),
- layer: 1,
- placeholder: `${$ls(`icu.enter_*`, {
- value: `${$ls(
- `common.profile_name`,
- )}`.toLowerCase(),
- })}`,
- field: form_fields.profile_name,
- callback: async ({ pass }) => {
- profile_name_valid = pass;
- },
- callback_keydown: async ({ key_s, el }) => {
- if (key_s) {
- el.blur();
- handle_continue();
- }
- },
- },
- }}
- />
<div
- class={`flex flex-row w-full gap-2 justify-center items-center`}
+ class={`flex flex-col h-[16rem] w-full px-4 gap-6 justify-start items-center`}
>
- <input
- type="checkbox"
- bind:checked={profile_name_nip05}
- />
- <button
- class={`flex flex-row justify-center items-center`}
- onclick={async () => {
- profile_name_nip05 = !profile_name_nip05;
- }}
+ <p class={`font-sans font-[600] text-ly0-gl text-3xl`}>
+ {`${$ls(`icu.add_*`, {
+ value: `${$ls(`common.profile`)}`,
+ })}`}
+ </p>
+ <div
+ class={`flex flex-col w-full gap-4 justify-center items-center`}
>
- <p
- class={`font-sans font-[400] text-ly0-gl text-[14px] tracking-wide`}
+ <EntryLine
+ bind:value={profile_name_val}
+ basis={{
+ loading: profile_name_loading,
+ wrap: {
+ layer: 1,
+ classes: `w-lo_${$app_lo}`,
+ style: `guide`,
+ },
+ el: {
+ classes: `font-sans text-[1.25rem] text-center placeholder:opacity-60`,
+ id: fmt_id(`nostr:profile`),
+ layer: 1,
+ placeholder: `${$ls(`icu.enter_*`, {
+ value: `${$ls(
+ `common.profile_name`,
+ )}`.toLowerCase(),
+ })}`,
+ field: form_fields.profile_name,
+ callback: async ({ pass }) => {
+ profile_name_valid = pass;
+ },
+ callback_keydown: async ({
+ key_s,
+ el,
+ }) => {
+ if (key_s) {
+ el.blur();
+ handle_continue();
+ }
+ },
+ },
+ }}
+ />
+ <div
+ class={`flex flex-row w-full gap-2 justify-center items-center`}
>
- {`${$ls(`common.create`)}`}
- <span
- class={`font-mono font-[500] tracking-tight px-[3px]`}
+ <input
+ type="checkbox"
+ bind:checked={profile_name_nip05}
+ />
+ <button
+ class={`flex flex-row justify-center items-center`}
+ onclick={async () => {
+ profile_name_nip05 =
+ !profile_name_nip05;
+ }}
>
- {`@radroots`}
- </span>
- {`${$ls(`common.nip05_address`)}`}
- </p>
- </button>
+ <p
+ class={`font-sans font-[400] text-ly0-gl text-[14px] tracking-wide`}
+ >
+ {`${$ls(`common.create`)}`}
+ <span
+ class={`font-mono font-[500] tracking-tight px-[3px]`}
+ >
+ {`@radroots`}
+ </span>
+ {`${$ls(`common.nip05_address`)}`}
+ </p>
+ </button>
+ </div>
+ </div>
</div>
- </div>
- </div>
- </CarouselItem>
- <CarouselItem
- basis={{
- view: `cfg_profile`,
- classes: `justify-center items-center`,
- role: `button`,
- tabindex: 0,
- callback_click: async () => {
- cfg_role = undefined;
- },
- }}
- >
- <div
- class={`flex flex-col h-[16rem] w-full gap-10 justify-start items-center`}
- >
- <div class={`flex flex-row w-full justify-center items-center`}>
- <p class={`font-sans font-[600] text-ly0-gl text-3xl`}>
- {`${$ls(`common.setup_for_farmer`)}`}
- </p>
- </div>
- <div
- class={`flex flex-col w-full gap-5 justify-center items-center`}
- >
- <button
- class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
- cfg_role === `farmer`
- ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
- : `bg-ly1`
- } el-re`}
- onclick={async (ev) => {
- ev.stopPropagation();
- cfg_role = `farmer`;
- }}
- >
- <p class={`font-sans font-[600] text-ly0-gl text-xl`}>
- {`${$ls(`common.yes`)}`}
- </p>
- </button>
- <button
- class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
- cfg_role === `personal`
- ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
- : `bg-ly1`
- } el-re`}
- onclick={async (ev) => {
- ev.stopPropagation();
- cfg_role = `personal`;
- }}
- >
- <p class={`font-sans font-[600] text-ly0-gl text-xl`}>
- {`${$ls(`common.no`)}`}
- </p>
- </button>
- </div>
- </div>
- </CarouselItem>
- </CarouselContainer>
- <div
- class={`absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`}
- >
- <ButtonLayoutPair
- basis={{
- continue: {
- label: `${$ls(`common.continue`)}`,
- disabled: $casl_i === 1 && !cfg_role,
- callback: async () => handle_continue(),
- },
- back: {
- visible: true,
- label:
- view === "cfg_profile" &&
- $casl_i === 0 &&
- !profile_name_val
- ? `${$ls(`common.skip`)}`
- : `${$ls(`common.back`)}`,
- callback: handle_back,
- },
- }}
- />
- </div>
-</div>
-
-<div
- data-view={`eula`}
- class={`hidden flex flex-col h-full w-full ios0:pt-12 pt-24 justify-start items-center`}
->
- <CarouselContainer
- basis={{
- view: `eula`,
- classes: `rounded-2xl scroll-hide`,
- }}
- >
- <CarouselItem
- basis={{
- view: `eula`,
- classes: `justify-start items-center`,
- }}
- >
- <div
- class={`flex flex-col h-full w-full px-4 justify-start items-center ${
- view === `eula` ? `fade-in-long` : ``
- }`}
- >
- <div
- class={`flex flex-col w-full px-4 gap-4 justify-start items-center`}
+ </CarouselItem>
+ <CarouselItem
+ basis={{
+ classes: `justify-center items-center`,
+ role: `button`,
+ tabindex: 0,
+ callback_click: async () => {
+ cfg_role = undefined;
+ },
+ }}
>
<div
- class={`flex flex-row w-full ios0:pt-8 justify-center items-center`}
- >
- <p class={`font-mono font-[600] text-ly0-gl text-2xl`}>
- {`${$ls(`eula.title`)}`}
- </p>
- </div>
- <div
- class={`flex flex-col ios0:h-[26rem] ios1:h-[38rem] w-full gap-6 justify-start items-center overflow-y-scroll scroll-hide`}
- onscroll={on_scroll_eula}
+ class={`flex flex-col h-[16rem] w-full gap-10 justify-start items-center`}
>
<div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
+ class={`flex flex-row w-full justify-center items-center`}
>
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.introduction.title`)}**`}
- </p>
<p
- class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ class={`font-sans font-[600] text-ly0-gl text-3xl`}
>
- {`${$ls(`eula.introduction.body`)}`}
+ {`${$ls(`common.setup_for_farmer`)}`}
</p>
</div>
<div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
+ class={`flex flex-col w-full gap-5 justify-center items-center`}
>
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.prohibited_content.title`)}**`}
- </p>
- <p
- class={`font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ <button
+ class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
+ cfg_role === `farmer`
+ ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
+ : `bg-ly1`
+ } el-re`}
+ onclick={async (ev) => {
+ ev.stopPropagation();
+ cfg_role = `farmer`;
+ }}
>
- {`${$ls(
- `eula.prohibited_content.body_0_title`,
- )}`}
- </p>
- <div
- class={`flex flex-col w-full justify-start items-start`}
+ <p
+ class={`font-sans font-[600] text-ly0-gl text-xl`}
+ >
+ {`${$ls(`common.yes`)}`}
+ </p>
+ </button>
+ <button
+ class={`flex flex-col h-bold_button w-lo_${$app_lo} justify-center items-center rounded-touch ${
+ cfg_role === `personal`
+ ? `ly1-apply-active ly1-raise-apply ly1-ring-apply`
+ : `bg-ly1`
+ } el-re`}
+ onclick={async (ev) => {
+ ev.stopPropagation();
+ cfg_role = `personal`;
+ }}
>
- {#each [0, 1, 2, 3, 4, 5] as li}
- <div
- class={`flex flex-row w-full justify-start items-center`}
- >
- <div
- class={`flex flex-row h-full w-8 justify-start items-start`}
- >
- <p
- class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
- >
- {`*`}
- </p>
- </div>
- <div
- class={`flex flex-row h-full w-full justify-start items-start`}
- >
- <p
- class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
- >
- {`${$ls(
- `eula.prohibited_content.body_li_0_${li}`,
- )}`}
- </p>
- </div>
- </div>
- {/each}
- </div>
+ <p
+ class={`font-sans font-[600] text-ly0-gl text-xl`}
+ >
+ {`${$ls(`common.no`)}`}
+ </p>
+ </button>
</div>
+ </div>
+ </CarouselItem>
+ </CarouselContainer>
+ <div
+ class={`absolute ios0:bottom-2 bottom-10 left-0 flex flex-col w-full justify-center items-center`}
+ >
+ <ButtonLayoutPair
+ basis={{
+ continue: {
+ label: `${$ls(`common.continue`)}`,
+ disabled:
+ $carousel_cfg_profile_index === 1 && !cfg_role,
+ callback: async () => handle_continue(),
+ },
+ back: {
+ visible: true,
+ label:
+ view === "cfg_profile" &&
+ $carousel_cfg_profile_index === 0 &&
+ !profile_name_val
+ ? `${$ls(`common.skip`)}`
+ : `${$ls(`common.back`)}`,
+ callback: handle_back,
+ },
+ }}
+ />
+ </div>
+ </div>
+ </ViewPane>
+
+ <ViewPane basis={{ view: "eula" }}>
+ <div
+ class={`flex flex-col h-full w-full ios0:pt-12 pt-24 justify-start items-center`}
+ >
+ <CarouselContainer
+ basis={{
+ carousel: carousel_eula,
+ classes: `rounded-2xl scroll-hide`,
+ }}
+ >
+ <CarouselItem
+ basis={{
+ classes: `justify-start items-center`,
+ }}
+ >
+ <div
+ class={`flex flex-col h-full w-full px-4 justify-start items-center ${
+ view === `eula` ? `fade-in-long` : ``
+ }`}
+ >
<div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
+ class={`flex flex-col w-full px-4 gap-4 justify-start items-center`}
>
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.prohibited_conduct.title`)}**`}
- </p>
<div
- class={`flex flex-col w-full justify-start items-start`}
+ class={`flex flex-row w-full ios0:pt-8 justify-center items-center`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl text-2xl`}
+ >
+ {`${$ls(`eula.title`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col ios0:h-[26rem] ios1:h-[38rem] w-full gap-6 justify-start items-center overflow-y-scroll scroll-hide`}
+ onscroll={on_scroll_eula}
>
- {#each [0, 1, 2, 3] as li}
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.introduction.title`)}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ >
+ {`${$ls(`eula.introduction.body`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.prohibited_content.title`)}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ >
+ {`${$ls(
+ `eula.prohibited_content.body_0_title`,
+ )}`}
+ </p>
<div
- class={`flex flex-row w-full justify-start items-center`}
+ class={`flex flex-col w-full justify-start items-start`}
>
- <div
- class={`flex flex-row h-full w-8 justify-start items-start`}
- >
- <p
- class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ {#each [0, 1, 2, 3, 4, 5] as li}
+ <div
+ class={`flex flex-row w-full justify-start items-center`}
>
- {`*`}
- </p>
- </div>
- <div
- class={`flex flex-row h-full w-full justify-start items-start`}
- >
- <p
- class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ <div
+ class={`flex flex-row h-full w-8 justify-start items-start`}
+ >
+ <p
+ class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ >
+ {`*`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-row h-full w-full justify-start items-start`}
+ >
+ <p
+ class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ >
+ {`${$ls(
+ `eula.prohibited_content.body_li_0_${li}`,
+ )}`}
+ </p>
+ </div>
+ </div>
+ {/each}
+ </div>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.prohibited_conduct.title`)}**`}
+ </p>
+ <div
+ class={`flex flex-col w-full justify-start items-start`}
+ >
+ {#each [0, 1, 2, 3] as li}
+ <div
+ class={`flex flex-row w-full justify-start items-center`}
>
- {`${$ls(
- `eula.prohibited_conduct.body_li_0_${li}`,
- )}`}
- </p>
- </div>
+ <div
+ class={`flex flex-row h-full w-8 justify-start items-start`}
+ >
+ <p
+ class={` font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ >
+ {`*`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-row h-full w-full justify-start items-start`}
+ >
+ <p
+ class={`col-span-10 font-mono font-[500] text-sm text-ly0-gl text-justify break-word`}
+ >
+ {`${$ls(
+ `eula.prohibited_conduct.body_li_0_${li}`,
+ )}`}
+ </p>
+ </div>
+ </div>
+ {/each}
</div>
- {/each}
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(
+ `eula.consequences_of_violation.title`,
+ )}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ >
+ {`${$ls(
+ `eula.consequences_of_violation.body`,
+ )}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.disclaimer.title`)}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ >
+ {`${$ls(`eula.disclaimer.body`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.changes.title`)}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ >
+ {`${$ls(`eula.changes.body`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.contact.title`)}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ >
+ {`${$ls(`eula.contact.body`)}`}
+ </p>
+ </div>
+ <div
+ class={`flex flex-col w-full gap-2 justify-start items-start`}
+ >
+ <p
+ class={`font-mono font-[600] text-ly0-gl`}
+ >
+ {`**${$ls(`eula.acceptance_of_terms.title`)}**`}
+ </p>
+ <p
+ class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ >
+ {`${$ls(`eula.acceptance_of_terms.body`)}`}
+ </p>
+ </div>
</div>
</div>
<div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
- >
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(
- `eula.consequences_of_violation.title`,
- )}**`}
- </p>
- <p
- class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
- >
- {`${$ls(
- `eula.consequences_of_violation.body`,
- )}`}
- </p>
- </div>
- <div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
+ class={`flex flex-row w-full ios0:pt-8 pt-6 justify-center items-center`}
>
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.disclaimer.title`)}**`}
- </p>
- <p
- class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
- >
- {`${$ls(`eula.disclaimer.body`)}`}
- </p>
- </div>
- <div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
- >
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.changes.title`)}**`}
- </p>
- <p
- class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
- >
- {`${$ls(`eula.changes.body`)}`}
- </p>
- </div>
- <div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
- >
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.contact.title`)}**`}
- </p>
- <p
- class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ <button
+ class={`group flex flex-row basis-1/2 gap-4 justify-center items-center ${
+ is_eula_scrolled ? `` : `opacity-80`
+ }`}
+ onclick={async () => {
+ const confirm = await notif.confirm({
+ message: `${$ls(
+ `eula.error.required`,
+ )}`,
+ cancel: `${$ls(`common.quit`)}`,
+ });
+
+ if (confirm === false)
+ await page_reset(undefined, true);
+ }}
>
- {`${$ls(`eula.contact.body`)}`}
- </p>
- </div>
- <div
- class={`flex flex-col w-full gap-2 justify-start items-start`}
- >
- <p class={`font-mono font-[600] text-ly0-gl`}>
- {`**${$ls(`eula.acceptance_of_terms.title`)}**`}
- </p>
- <p
- class={`font-mono font-[500] text-ly0-gl text-sm text-justify break-word`}
+ <p
+ class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`}
+ >
+ {`-`}
+ </p>
+ <p
+ class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`}
+ >
+ {`${`${$ls(`common.disagree`)}`}`}
+ </p>
+ <p
+ class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`}
+ >
+ {`-`}
+ </p>
+ </button>
+ <button
+ class={`relative group flex flex-row basis-1/2 gap-4 justify-center items-center el-re ${
+ is_eula_scrolled ? `` : `opacity-40`
+ }`}
+ onclick={async () => {
+ if (is_eula_scrolled) await submit();
+ }}
>
- {`${$ls(`eula.acceptance_of_terms.body`)}`}
- </p>
+ <p
+ class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`}
+ >
+ {`-`}
+ </p>
+ <p
+ class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`}
+ >
+ {`${`${$ls(`common.agree`)}`}`}
+ </p>
+ <p
+ class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`}
+ >
+ {`- `}
+ </p>
+ {#if is_loading_s}
+ <div
+ class={`absolute right-3 flex flex-row justify-start items-center`}
+ >
+ <LoadSymbol basis={{ dim: `xs` }} />
+ </div>
+ {/if}
+ </button>
</div>
</div>
- </div>
- <div
- class={`flex flex-row w-full ios0:pt-8 pt-6 justify-center items-center`}
- >
- <button
- class={`group flex flex-row basis-1/2 gap-4 justify-center items-center ${
- is_eula_scrolled ? `` : `opacity-80`
- }`}
- onclick={async () => {
- const confirm = await notif.confirm({
- message: `${$ls(`eula.error.required`)}`,
- cancel: `${$ls(`common.quit`)}`,
- });
-
- if (confirm === false)
- await page_reset(undefined, true);
- }}
- >
- <p
- class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`}
- >
- {`-`}
- </p>
- <p
- class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`}
- >
- {`${`${$ls(`common.disagree`)}`}`}
- </p>
- <p
- class={`font-mono font-[400] text-sm text-ly0-gl group-active:text-ly0-gl el-re`}
- >
- {`-`}
- </p>
- </button>
- <button
- class={`relative group flex flex-row basis-1/2 gap-4 justify-center items-center el-re ${
- is_eula_scrolled ? `` : `opacity-40`
- }`}
- onclick={async () => {
- if (is_eula_scrolled) await submit();
- }}
- >
- <p
- class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`}
- >
- {`-`}
- </p>
- <p
- class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`}
- >
- {`${`${$ls(`common.agree`)}`}`}
- </p>
- <p
- class={`font-mono font-[400] text-sm text-ly0-gl-hl group-active:text-ly0-gl-hl/80 el-re`}
- >
- {`- `}
- </p>
- {#if is_loading_s}
- <div
- class={`absolute right-3 flex flex-row justify-start items-center`}
- >
- <LoadSymbol basis={{ dim: `xs` }} />
- </div>
- {/if}
- </button>
- </div>
- </div>
- </CarouselItem>
- </CarouselContainer>
-</div>
+ </CarouselItem>
+ </CarouselContainer>
+ </div>
+ </ViewPane>
+</ViewStack>
diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
import { dev, version as kit_version } from "$app/environment";
import { page } from "$app/state";
- import { db, geoc } from "$lib/utils/app";
+ import { db_init, geoc_init } from "$lib/utils/app";
import { app_cfg } from "$lib/utils/app/config";
import {
lc_color_mode,
@@ -23,7 +23,8 @@
} from "@radroots/apps-lib";
import { Css, LayoutWindow } from "@radroots/apps-lib-pwa";
import { app_lo } from "@radroots/apps-lib-pwa/stores/app";
- import { cfg_app } from "@radroots/apps-lib-pwa/utils/app";
+ import type { LibContext } from "@radroots/apps-lib-pwa/types/context";
+ import { CFG_APP } from "@radroots/apps-lib-pwa/utils/app";
import { parse_theme_key, parse_theme_mode } from "@radroots/themes";
import { str_cap_words } from "@radroots/utils";
import "css-paint-polyfill";
@@ -36,7 +37,7 @@
content: string;
};
- const head_meta_tags: MetaTag[] = [
+ const HEAD_META_TAGS: MetaTag[] = [
{
name: "app_version",
content: app_cfg.version,
@@ -57,7 +58,7 @@
let { children }: LayoutProps = $props();
- set_context("lib", {
+ const LIB_CONTEXT: LibContext = {
ls,
locale,
lc_color_mode,
@@ -68,7 +69,9 @@
lc_img_bin,
lc_photos_add,
lc_photos_upload,
- });
+ };
+
+ set_context("lib", LIB_CONTEXT);
theme_mode.subscribe((_theme_mode) =>
theme_set(parse_theme_key($theme_key), parse_theme_mode(_theme_mode)),
@@ -79,16 +82,28 @@
);
win_h.subscribe((_win_h) => {
- if (_win_h > cfg_app.layout.ios1.h) {
+ if (_win_h > CFG_APP.layout.ios1.h) {
app_lo.set("ios1");
} else {
app_lo.set("ios0");
}
});
+ const register_service_worker = async (): Promise<void> => {
+ if (dev) return;
+ if (!("serviceWorker" in navigator)) return;
+ try {
+ await navigator.serviceWorker.register("/service-worker.js");
+ await navigator.serviceWorker.ready;
+ } catch {
+ return;
+ }
+ };
+
onMount(async () => {
- await db.init();
- await geoc.connect();
+ await db_init();
+ await geoc_init();
+ await register_service_worker();
});
const format_title = (title: string): string => {
@@ -100,7 +115,7 @@
<svelte:head>
<title>{`${head_title || "Home"} | Rad Roots`}</title>
- {#each head_meta_tags as meta_tag (meta_tag.name)}
+ {#each HEAD_META_TAGS as meta_tag (meta_tag.name)}
<meta name={meta_tag.name} content={meta_tag.content} />
{/each}
</svelte:head>
diff --git a/app/src/service-worker.js b/app/src/service-worker.js
@@ -1,9 +1,86 @@
-import { build, files, prerendered, version } from '$service-worker'
-//import { precacheAndRoute } from 'workbox-precaching'
+import { build, files, prerendered, version } from "$service-worker";
-const precache_list = [...build, ...files, ...prerendered].map((s) => ({
- url: s,
- revision: version,
-}))
+const APP_SHELL_URL = "/index.html";
+const PRECACHE_URLS = [...new Set([...build, ...files, ...prerendered, APP_SHELL_URL])].filter(
+ (url) => !url.includes("/.")
+);
+const PRECACHE_LIST = PRECACHE_URLS.map((url) => ({
+ url,
+ revision: version
+}));
+const APP_CACHE = `cache-app-shell-v${version}`;
+const APP_CACHE_PREFIX = "cache-app-shell-v";
-//precacheAndRoute(precache_list)
-\ No newline at end of file
+const precache = async () => {
+ const cache = await caches.open(APP_CACHE);
+ await cache.addAll(PRECACHE_LIST.map((entry) => entry.url));
+};
+
+const cleanup_caches = async () => {
+ const keys = await caches.keys();
+ for (const key of keys) {
+ if (!key.startsWith(APP_CACHE_PREFIX)) continue;
+ if (key === APP_CACHE) continue;
+ await caches.delete(key);
+ }
+};
+
+const range_response = async (request, response) => {
+ const range = request.headers.get("range");
+ if (!range || !response) return response;
+ const bytes = /bytes=(\d+)-(\d+)?/u.exec(range);
+ if (!bytes) return response;
+ const start = Number(bytes[1]);
+ const end_raw = bytes[2];
+ const buffer = await response.arrayBuffer();
+ const end = end_raw ? Number(end_raw) : buffer.byteLength - 1;
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return response;
+ const sliced = buffer.slice(start, end + 1);
+ const headers = new Headers(response.headers);
+ headers.set("Content-Range", `bytes ${start}-${end}/${buffer.byteLength}`);
+ headers.set("Content-Length", `${sliced.byteLength}`);
+ return new Response(sliced, { status: 206, statusText: "Partial Content", headers });
+};
+
+const cache_first = async (request) => {
+ const cache = await caches.open(APP_CACHE);
+ const cached = await cache.match(request);
+ if (cached) return await range_response(request, cached);
+ const response = await fetch(request);
+ if (response.ok) await cache.put(request, response.clone());
+ return response;
+};
+
+const network_first = async (request) => {
+ const cache = await caches.open(APP_CACHE);
+ try {
+ const response = await fetch(request);
+ if (response.ok) await cache.put(request, response.clone());
+ return response;
+ } catch {
+ const cached = await cache.match(request);
+ if (cached) return await range_response(request, cached);
+ const fallback = await cache.match(APP_SHELL_URL);
+ if (fallback) return fallback;
+ return new Response("offline", { status: 503 });
+ }
+};
+
+self.addEventListener("install", (event) => {
+ event.waitUntil(precache().then(() => self.skipWaiting()));
+});
+
+self.addEventListener("activate", (event) => {
+ event.waitUntil(cleanup_caches().then(() => self.clients.claim()));
+});
+
+self.addEventListener("fetch", (event) => {
+ const request = event.request;
+ if (request.method !== "GET") return;
+ const url = new URL(request.url);
+ if (request.mode === "navigate") {
+ event.respondWith(network_first(request));
+ return;
+ }
+ if (url.origin === self.location.origin) event.respondWith(cache_first(request));
+});
diff --git a/package.json b/package.json
@@ -6,8 +6,8 @@
"build": "turbo build",
"build:app": "turbo build --filter=app",
"build:packages": "turbo run build --filter=./packages/*",
- "dev:app": "turbo dev --filter=app",
- "dev:packages": "turbo dev --filter=./packages/* --concurrency 20"
+ "dev:app": "turbo dev --filter=app --filter=./packages/* --concurrency 25",
+ "dev:packages": "turbo dev --filter=./packages/* --concurrency 25"
},
"devDependencies": {
"turbo": "2.5.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -447,6 +447,9 @@ importers:
'@radroots/locales':
specifier: workspace:*
version: link:../packages/locales
+ '@radroots/nfc':
+ specifier: workspace:*
+ version: link:../packages/nfc
'@radroots/nostr':
specifier: workspace:*
version: link:../packages/nostr
@@ -765,6 +768,22 @@ importers:
specifier: 5.8.3
version: 5.8.3
+ packages/ble:
+ dependencies:
+ '@radroots/utils':
+ specifier: workspace:*
+ version: link:../utils
+ devDependencies:
+ '@radroots/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+ rimraf:
+ specifier: ^6.0.1
+ version: 6.0.1
+ typescript:
+ specifier: 5.8.3
+ version: 5.8.3
+
packages/client:
dependencies:
'@radroots/geo':
@@ -916,6 +935,22 @@ importers:
specifier: 5.8.3
version: 5.8.3
+ packages/nfc:
+ dependencies:
+ '@radroots/utils':
+ specifier: workspace:*
+ version: link:../utils
+ devDependencies:
+ '@radroots/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+ rimraf:
+ specifier: ^6.0.1
+ version: 6.0.1
+ typescript:
+ specifier: 5.8.3
+ version: 5.8.3
+
packages/nostr:
dependencies:
'@noble/curves':
@@ -5318,7 +5353,7 @@ snapshots:
'@babel/types': 7.28.5
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -5370,7 +5405,7 @@ snapshots:
'@babel/core': 7.28.5
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-plugin-utils': 7.27.1
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
lodash.debounce: 4.0.8
resolve: 1.22.11
transitivePeerDependencies:
@@ -5942,7 +5977,7 @@ snapshots:
'@babel/parser': 7.28.5
'@babel/template': 7.27.2
'@babel/types': 7.28.5
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -6804,7 +6839,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
svelte: 5.46.0
vite: 7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)
transitivePeerDependencies:
@@ -6813,7 +6848,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
svelte: 5.46.0
vite: 7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)
transitivePeerDependencies:
@@ -6822,7 +6857,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
svelte: 5.46.0
vite: 7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)
transitivePeerDependencies:
@@ -6831,7 +6866,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.21
@@ -6845,7 +6880,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
deepmerge: 4.3.1
magic-string: 0.30.21
svelte: 5.46.0
@@ -6857,7 +6892,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)))(svelte@5.46.0)(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
deepmerge: 4.3.1
magic-string: 0.30.21
svelte: 5.46.0
@@ -7431,10 +7466,6 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
- debug@4.4.3:
- dependencies:
- ms: 2.1.3
-
debug@4.4.3(supports-color@8.1.1):
dependencies:
ms: 2.1.3
@@ -9249,7 +9280,7 @@ snapshots:
bundle-require: 4.2.1(esbuild@0.17.19)
cac: 6.7.14
chokidar: 3.6.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
esbuild: 0.17.19
execa: 5.1.1
globby: 11.1.0
@@ -9395,7 +9426,7 @@ snapshots:
vite-plugin-pwa@1.2.0(vite@7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0))(workbox-build@7.4.0)(workbox-window@7.4.0):
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.15
vite: 7.0.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)