commit 4ab7f818ce0a32c1827191c125bed351e256bd69 parent 02189825ba83c5e3e75123bf80b7e281c66174b5 Author: triesap <triesap@radroots.dev> Date: Mon, 17 Nov 2025 23:53:16 +0000 apps-lib-pwa: migrate code from `@radroots/apps-lib`, including scaffolding, keystore client, ui layouts Diffstat:
| A | apps-lib-pwa/.env.example | | | 4 | ++++ |
| M | apps-lib-pwa/.gitignore | | | 54 | ++++++++++++++++++++++++++---------------------------- |
| D | apps-lib-pwa/README.md | | | 2 | -- |
| M | apps-lib-pwa/package.json | | | 26 | +++++++++++++++++++++++--- |
| D | apps-lib-pwa/src/global.d.ts | | | 3 | --- |
| A | apps-lib-pwa/src/lib/client/keystore-nostr.ts | | | 94 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/button-label-dashed.svelte | | | 22 | ++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/button-layout-pair.svelte | | | 66 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/button-layout.svelte | | | 53 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/button-round-nav.svelte | | | 26 | ++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/button-simple.svelte | | | 22 | ++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/glyph-button-simple.svelte | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/buttons/glyph-button.svelte | | | 18 | ++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/forms/entry-line.svelte | | | 70 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/forms/entry-wrap.svelte | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/forms/form-line-ledger-label-select-label.svelte | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/forms/form-line-ledger-select.svelte | | | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/forms/form-line-ledger.svelte | | | 88 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/layouts/layout-bottom-button.svelte | | | 22 | ++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/layouts/layout-page.svelte | | | 18 | ++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/layouts/layout-trellis.svelte | | | 18 | ++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/layouts/layout-view.svelte | | | 48 | ++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/layouts/layout-window.svelte | | | 35 | +++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/css.svelte | | | 3 | +++ |
| A | apps-lib-pwa/src/lib/components/lib/float-page.svelte | | | 12 | ++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/glyph-circle.svelte | | | 21 | +++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/image-upload-add-photo.svelte | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/input-pwa.svelte | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/input-value.svelte | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/label-swap.svelte | | | 41 | +++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/load-symbol.svelte | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/logo-circle-sm.svelte | | | 14 | ++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/logo-circle.svelte | | | 18 | ++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/logo-letters.svelte | | | 3 | +++ |
| A | apps-lib-pwa/src/lib/components/lib/select-menu.svelte | | | 79 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/select-pwa.svelte | | | 126 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/lib/wrap-border.svelte | | | 22 | ++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/map/map-marker-area-display.svelte | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/map/map-marker-area.svelte | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/map/map.svelte | | | 42 | ++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/navigation/navigation-tabs.svelte | | | 100 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/navigation/page-header.svelte | | | 55 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/navigation/page-toolbar.svelte | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-default-label.svelte | | | 37 | +++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-end.svelte | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-input.svelte | | | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-line.svelte | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-offset.svelte | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-row-display-value.svelte | | | 44 | ++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-row-label.svelte | | | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-select.svelte | | | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-title.svelte | | | 69 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis-touch.svelte | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/components/trellis/trellis.svelte | | | 149 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/features/farm/farms-add-casli-detail.svelte | | | 98 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/features/farm/farms-add-casli-map.svelte | | | 94 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/features/farm/farms-display-li-el.svelte | | | 109 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/features/farm/farms-products-review-card.svelte | | | 124 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | apps-lib-pwa/src/lib/index.ts | | | 75 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| A | apps-lib-pwa/src/lib/stores/app.ts | | | 33 | +++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/stores/nostr.ts | | | 16 | ++++++++++++++++ |
| A | apps-lib-pwa/src/lib/styles/lib.ts | | | 10 | ++++++++++ |
| A | apps-lib-pwa/src/lib/types/app.ts | | | 28 | ++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/types/components.ts | | | 33 | +++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/types/components/trellis.ts | | | 124 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/types/views.ts | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/utils/app.ts | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/utils/context.ts | | | 22 | ++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/utils/map.ts | | | 25 | +++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/utils/schemas/farm.ts | | | 29 | +++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/utils/schemas/location-gcs.ts | | | 23 | +++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/farms/farm.ts | | | 15 | +++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/farms/farms-add.svelte | | | 223 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/farms/farms-details.svelte | | | 247 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/farms/farms.svelte | | | 84 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/home.svelte | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/profile/profile.svelte | | | 305 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | apps-lib-pwa/src/lib/views/profile/types.ts | | | 12 | ++++++++++++ |
| A | apps-lib-pwa/src/lib/views/settings.svelte | | | 109 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | apps-lib-pwa/vite.config.ts | | | 2 | +- |
80 files changed, 4555 insertions(+), 39 deletions(-)
diff --git a/apps-lib-pwa/.env.example b/apps-lib-pwa/.env.example @@ -0,0 +1,4 @@ +VITE_PUBLIC_IDB_NAME= +VITE_PUBLIC_NDK_CACHE_NAME= +VITE_PUBLIC_NDK_CLIENT_NAME= +VITE_PUBLIC_RADROOTS_MARKET_RELAY_URL= diff --git a/apps-lib-pwa/.gitignore b/apps-lib-pwa/.gitignore @@ -1,18 +1,10 @@ node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build dist .turbo -# OS -.DS_Store -Thumbs.db +# Logs +logs/ +*.log # Env .env @@ -20,26 +12,31 @@ Thumbs.db !.env.example !.env.test -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* -vite.config.dev* - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# OS +.DS_Store +Thumbs.db -# secrets +# Secrets *.pem +*.crt +*.key + +# Testing +test*.json -# local -.tmp* -.backup* -.dev* -.vscode +# Editors +.vscode/ +.idea/ +*.iml + +# Notes notes*.txt notes*.md notes*.json -git-diff*.txt -justfile +tree*.txt +diff*.txt +prompt*.txt + +# Dev +.local* +justfile +\ No newline at end of file diff --git a/apps-lib-pwa/README.md b/apps-lib-pwa/README.md @@ -1 +0,0 @@ -# apps lib native -\ No newline at end of file diff --git a/apps-lib-pwa/package.json b/apps-lib-pwa/package.json @@ -1,13 +1,14 @@ { - "name": "@radroots/apps-lib-native", + "name": "@radroots/apps-lib-pwa", "version": "0.0.0", "private": true, "license": "GPLv3", "scripts": { "dev": "svelte-package -w", - "prebuild": "npm run check", + "prebuild": "npm run clean && npm run check", "build": "svelte-package", "preview": "vite preview", + "clean": "rimraf dist", "prepare": "svelte-kit sync || echo ''", "prepack": "svelte-kit sync && svelte-package && publint", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -46,5 +47,24 @@ "typescript": "5.8.3", "vite": "7.0.6" }, - "dependencies": {} + "dependencies": { + "@radroots/locales": "*", + "@radroots/apps-lib": "*", + "@radroots/events-bindings": "*", + "@radroots/client": "*", + "@radroots/tangle-schema-bindings": "*", + "@radroots/themes": "*", + "@radroots/utils": "*", + "@radroots/utils-nostr": "*", + "@nostr-dev-kit/ndk": "2.14.33", + "@nostr-dev-kit/ndk-cache-dexie": "2.6.34", + "@nostr-dev-kit/ndk-svelte": "2.4.38", + "@sveltekit-i18n/base": "^1.3.7", + "@sveltekit-i18n/parser-icu": "^1.0.8", + "luxon": "^3.5.0", + "sveltekit-i18n": "^2.4.2", + "svelte-maplibre": "1.2.0", + "sveltekit-search-params": "^3.0.0", + "zod": "^4.0.5" + } } \ No newline at end of file diff --git a/apps-lib-pwa/src/global.d.ts b/apps-lib-pwa/src/global.d.ts @@ -1,3 +0,0 @@ -declare module "$app/environment"; -declare module "$app/navigation"; -declare module "$app/stores"; diff --git a/apps-lib-pwa/src/lib/client/keystore-nostr.ts b/apps-lib-pwa/src/lib/client/keystore-nostr.ts @@ -0,0 +1,94 @@ +import { keystore } from "@radroots/client"; +import { err_msg, handle_err, type ResolveError, type ResultObj, type ResultPass, type ResultPublicKey, type ResultSecretKey, type ResultsList } from '@radroots/utils'; +import { lib_nostr_key_generate, lib_nostr_public_key, lib_nostr_secret_key_validate } from '@radroots/utils-nostr'; + +export type INostrKeystore = { + generate(): Promise<ResolveError<ResultPublicKey>>; + add(secret_key: string): Promise<ResolveError<ResultPublicKey>>; + read(public_key: string): Promise<ResolveError<ResultSecretKey>>; + keys(): Promise<ResolveError<ResultsList<string>>>; + remove(public_key: string): Promise<ResolveError<ResultObj<string>>>; + reset(): Promise<ResolveError<ResultPass>>; +}; + +export class PwaKeystoreNostr implements INostrKeystore { + private static readonly keystore_database = "radroots-pwa-v1-keystore"; + private static readonly keystore_store = "nostr"; + + private _keystore: keystore.WebKeystore; + + constructor() { + this._keystore = new keystore.WebKeystore({ + database: PwaKeystoreNostr.keystore_database, + store: PwaKeystoreNostr.keystore_store, + }); + } + + private async add_secret_key(secret_key_raw: string) { + const secret_key = lib_nostr_secret_key_validate(secret_key_raw); + if (!secret_key) throw new Error("error.nostr.invalid_secret_key"); + const public_key = lib_nostr_public_key(secret_key); + return await this._keystore.add(public_key, secret_key); + } + + public async generate() { + try { + const secret_key = lib_nostr_key_generate(); + const resolve = await this.add_secret_key(secret_key); + if ("err" in resolve) return resolve; + return { public_key: resolve.result }; + } catch (e) { + return handle_err(e); + } + }; + + public async add(secret_key_raw: string) { + try { + const resolve = await this.add_secret_key(secret_key_raw); + if ("err" in resolve) return resolve; + return { public_key: resolve.result }; + } catch (e) { + return handle_err(e); + } + }; + + public async read(public_key?: string) { + try { + const resolve = await this._keystore.read(public_key); + if ("err" in resolve) return resolve; + return { secret_key: resolve.result }; + } catch (e) { + return handle_err(e); + } + }; + + public async keys() { + try { + const resolve = await this._keystore.keys(); + if ("err" in resolve) return resolve; + if (resolve.results.length) return resolve; + return err_msg("error.client.keystore-nostr.no_results"); + } catch (e) { + return handle_err(e); + } + }; + + public async remove(public_key: string) { + try { + const resolve = await this._keystore.remove(public_key); + if ("err" in resolve) return resolve; + return { result: public_key }; + } catch (e) { + return handle_err(e); + } + }; + + public async reset() { + try { + const resolve = await this._keystore.reset(); + return resolve; + } catch (e) { + return handle_err(e); + } + }; +} diff --git a/apps-lib-pwa/src/lib/components/buttons/button-label-dashed.svelte b/apps-lib-pwa/src/lib/components/buttons/button-label-dashed.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import type { ICb } from "@radroots/apps-lib"; + + let { + basis, + }: { + basis: ICb & { + label: string; + }; + } = $props(); +</script> + +<button + class={`flex flex-row h-line_button w-full justify-center items-center`} + onclick={async () => { + await basis.callback(); + }} +> + <p class={`font-sans font-[500] text-lg text-ly0-gl-hl tracking-wide`}> + {`- ${basis.label} -`} + </p> +</button> diff --git a/apps-lib-pwa/src/lib/components/buttons/button-layout-pair.svelte b/apps-lib-pwa/src/lib/components/buttons/button-layout-pair.svelte @@ -0,0 +1,66 @@ +<script lang="ts"> + import { app_lo } from "$lib/stores/app"; + import { + Flex, + fmt_cl, + type IClOpt, + type IDisabledOpt, + type ILoadingOpt, + } from "@radroots/apps-lib"; + import { type CallbackPromise } from "@radroots/utils"; + import ButtonLayout from "./button-layout.svelte"; + + let { + basis, + }: { + basis: IClOpt & { + continue: IDisabledOpt & + ILoadingOpt & { + label: string; + callback: CallbackPromise; + }; + back?: IDisabledOpt & { + visible: boolean; + label?: string; + callback: CallbackPromise; + }; + }; + } = $props(); +</script> + +<div + class={`${fmt_cl(basis.classes)} flex flex-col gap-1 justify-center items-center ${basis?.back?.visible ? `-translate-y-8` : ``} el-re`} +> + <ButtonLayout + basis={{ + disabled: basis.continue.disabled, + label: basis.continue.label, + callback: basis.continue.callback, + }} + /> + {#if basis.back} + <div class={`flex flex-col justify-center items-center el-re`}> + {#if basis.back.visible} + <button + class={`group flex flex-row h-12 w-lo_${$app_lo} justify-center items-center fade-in el-re`} + onclick={async (ev) => { + ev.stopPropagation(); + if (!basis.back?.disabled) await basis.back?.callback(); + }} + > + <p + class={`font-sans font-[600] tracking-wide text-ly1-gl-shade ${basis.back?.disabled ? `` : `group-active:text-ly1-gl/40`} el-re`} + > + {basis.back.label || ``} + </p> + </button> + {:else} + <div + class={`flex flex-row h-4 w-full justify-start items-center`} + > + <Flex /> + </div> + {/if} + </div> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/buttons/button-layout.svelte b/apps-lib-pwa/src/lib/components/buttons/button-layout.svelte @@ -0,0 +1,53 @@ +<script lang="ts"> + import { app_lo } from "$lib/stores/app"; + import { + type IClOpt, + type IDisabledOpt, + type ILoadingOpt, + type ILyOpt, + fmt_cl, + parse_layer, + } from "@radroots/apps-lib"; + import type { CallbackPromise } from "@radroots/utils"; + import LoadSymbol from "../lib/load-symbol.svelte"; + + let { + basis, + }: { + basis: ILyOpt & + IClOpt & + IDisabledOpt & + ILoadingOpt & { + classes_inner?: string; + hide_active?: boolean; + label: string; + callback: CallbackPromise; + }; + } = $props(); + + const layer = $derived(parse_layer(basis.layer, 1)); + + const classes_active = $derived( + !basis.hide_active + ? `ly1-active-surface ly1-active-raise-less ly1-active-ring-less` + : ``, + ); +</script> + +<button + class={`${fmt_cl(basis.classes)} group flex flex-row h-touch_guide w-lo_${$app_lo} justify-center items-center bg-ly${layer} rounded-touch ${basis.disabled ? `opacity-60` : classes_active} el-re`} + onclick={async (ev) => { + ev.stopPropagation(); + if (!basis.disabled) await basis.callback(); + }} +> + {#if basis.loading} + <LoadSymbol basis={{ dim: `md` }} /> + {:else} + <p + class={`${fmt_cl(basis.classes_inner)} font-sans font-[600] tracking-wide text-ly${layer}-gl-shade ${basis.disabled ? `` : `group-active:text-ly${layer}-gl/40 `} el-re`} + > + {basis.label || ``} + </p> + {/if} +</button> diff --git a/apps-lib-pwa/src/lib/components/buttons/button-round-nav.svelte b/apps-lib-pwa/src/lib/components/buttons/button-round-nav.svelte @@ -0,0 +1,26 @@ +<script lang="ts"> + import type { IButtonNavRound } from "$lib/types/components"; + import { Glyph } from "@radroots/apps-lib"; + import LoadSymbol from "../lib/load-symbol.svelte"; + + let { basis }: { basis: IButtonNavRound } = $props(); +</script> + +<button + class={`flex flex-row h-12 w-12 justify-center items-center bg-ly1 rounded-full el-re`} + disabled={!!basis.disabled} + onclick={basis.callback} +> + {#if basis.loading} + <LoadSymbol /> + {:else} + <Glyph + basis={{ + classes: `text-ly0-gl`, + dim: `sm+`, + + key: basis.glyph, + }} + /> + {/if} +</button> diff --git a/apps-lib-pwa/src/lib/components/buttons/button-simple.svelte b/apps-lib-pwa/src/lib/components/buttons/button-simple.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import type { IButtonSimple } from "$lib/types/components"; + import { parse_layer } from "@radroots/apps-lib"; + + let { basis }: { basis: IButtonSimple } = $props(); + + const layer = $derived(parse_layer(basis.layer || 1)); +</script> + +<button + class={`group flex flex-row h-line_button w-full justify-center items-center rounded-touch bg-ly${layer} ly${layer}-active-surface ly${layer}-active-ring`} + onclick={async (ev) => { + if (!basis.allow_propogation) ev.stopPropagation(); + await basis.callback(); + }} +> + <p + class={`font-sans font-[600] text-xl text-ly0-gl capitalize tracking-wider opacity-active`} + > + {basis.label} + </p> +</button> diff --git a/apps-lib-pwa/src/lib/components/buttons/glyph-button-simple.svelte b/apps-lib-pwa/src/lib/components/buttons/glyph-button-simple.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import { + type GlyphKey, + type IClOpt, + fmt_cl, + Glyph, + } from "@radroots/apps-lib"; + import { type CallbackPromise } from "@radroots/utils"; + + let { + basis, + }: { + basis: IClOpt & { + kind?: `primary` | `neutral`; + label: string; + callback: CallbackPromise; + glyph?: GlyphKey; + }; + } = $props(); + + const classes_kind = $derived( + basis.kind === `neutral` ? `text-ly0-gl-shade` : `text-ly0-gl-hl`, + ); +</script> + +<button + class={`${fmt_cl(basis.classes)} group flex flex-row justify-center items-center`} + onclick={basis.callback} +> + {#if basis.glyph} + <Glyph + basis={{ + classes: `${classes_kind}`, + dim: `sm+`, + + key: basis.glyph, + }} + /> + {/if} + <p + class={`font-sans font-[600] text-line_label ${classes_kind} opacity-active`} + > + {basis.label} + </p> +</button> diff --git a/apps-lib-pwa/src/lib/components/buttons/glyph-button.svelte b/apps-lib-pwa/src/lib/components/buttons/glyph-button.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import { type IGlyph, fmt_cl, glyph_style_map } from "@radroots/apps-lib"; + + let { basis }: { basis: IGlyph } = $props(); + const styles = $derived( + basis?.dim ? glyph_style_map.get(basis.dim) : glyph_style_map.get(`sm`), + ); +</script> + +<!-- svelte-ignore a11y_consider_explicit_label --> +<button + class={`${fmt_cl(basis.classes)} flex flex-col justify-center items-center text-[${styles?.gl_1}px] el-re`} + onclick={async () => { + if (basis.callback) await basis.callback(); + }} +> + <i class={`ph-bold ph-${basis.key}`}></i> +</button> diff --git a/apps-lib-pwa/src/lib/components/forms/entry-line.svelte b/apps-lib-pwa/src/lib/components/forms/entry-line.svelte @@ -0,0 +1,70 @@ +<script lang="ts"> + import { + Glyph, + parse_layer, + type IEntryLine, + type LoadingDimension, + } from "@radroots/apps-lib"; + import InputValue from "../lib/input-value.svelte"; + import LoadSymbol from "../lib/load-symbol.svelte"; + import EntryWrap from "./entry-wrap.svelte"; + + let { + basis, + value = $bindable(``), + }: { + basis: IEntryLine; + value: string; + } = $props(); + + const layer = $derived( + parse_layer( + typeof basis.wrap?.layer === `boolean` ? 0 : basis.wrap?.layer, + ), + ); + + const classes_layer = $derived( + typeof basis.wrap?.layer === `boolean` ? `` : `text-ly${layer}-gl`, + ); + + const loading_dim: LoadingDimension = $derived( + basis.wrap?.style === `guide` ? `md` : `sm`, + ); +</script> + +<EntryWrap basis={basis?.wrap}> + <InputValue + bind:value + basis={{ + ...basis.el, + classes: `h-entry_line ${basis.el.classes}`, + }} + /> + {#if basis.loading} + <div + class={`z-5 absolute right-0 top-0 flex flex-row h-full pr-4 justify-end items-center fade-in el-re`} + > + <LoadSymbol + basis={{ + dim: loading_dim, + }} + /> + </div> + {:else if basis.notify_inline} + {#if `glyph` in basis.notify_inline} + <div + class={`z-5 absolute el-re right-0 top-0 flex flex-row h-full pr-3 justify-end items-center translate-x-[34px] fade-in`} + > + <Glyph + basis={typeof basis.notify_inline.glyph === `string` + ? { + key: basis.notify_inline.glyph, + dim: `xs+`, + classes: `${classes_layer}`, + } + : basis.notify_inline.glyph} + /> + </div> + {/if} + {/if} +</EntryWrap> diff --git a/apps-lib-pwa/src/lib/components/forms/entry-wrap.svelte b/apps-lib-pwa/src/lib/components/forms/entry-wrap.svelte @@ -0,0 +1,36 @@ +<script lang="ts"> + import { + fmt_cl, + type IBasisOpt, + type IEntryWrap, + parse_layer, + } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis = undefined, + children, + }: { + basis?: IBasisOpt<IEntryWrap>; + children: Snippet; + } = $props(); + + const layer = $derived( + typeof basis?.layer === `boolean` + ? parse_layer(0) + : parse_layer(basis?.layer), + ); + + const classes_layer = $derived( + typeof basis?.layer === `boolean` + ? `bg-transparent` + : `bg-ly${layer} ${basis?.style_a ? `active:bg-ly${layer}-a` : ``}`, + ); +</script> + +<button + id={basis?.id || null} + class={`${fmt_cl(basis?.classes)} relative entry-line-wrap ${!basis?.no_pad ? ` pl-6 pr-4` : ``} h-entry_${basis?.style ? basis.style : `line`} rounded-touch ${classes_layer} el-re`} +> + {@render children()} +</button> diff --git a/apps-lib-pwa/src/lib/components/forms/form-line-ledger-label-select-label.svelte b/apps-lib-pwa/src/lib/components/forms/form-line-ledger-label-select-label.svelte @@ -0,0 +1,36 @@ +<script lang="ts"> + import { symbols } from "@radroots/apps-lib"; + + let { + basis, + }: { + basis: { + label: string; + }; + } = $props(); +</script> + +<div class={`flex flex-row justify-start items-center`}> + <p + class={`pr-[13px] font-sansd text-trellis_ti text-ly0-gl-label uppercase`} + > + {`(${basis.label}`} + </p> + <div + class={`relative flex flex-row justify-start items-center -translate-x-[10px] -translate-y-[1px]`} + > + <p + class={`absolute font-sansd text-trellis_ti text-ly0-gl-label uppercase scale-y-[70%] scale-x-[80%] -translate-y-[1px]`} + > + {`${symbols.up}`} + </p> + <p + class={`absolute font-sansd text-trellis_ti text-ly0-gl-label uppercase scale-y-[70%] scale-x-[80%] translate-y-[2px]`} + > + {`${symbols.down}`} + </p> + </div> + <p class={`font-sansd text-trellis_ti text-ly0-gl-label uppercase`}> + {`)`} + </p> +</div> diff --git a/apps-lib-pwa/src/lib/components/forms/form-line-ledger-select.svelte b/apps-lib-pwa/src/lib/components/forms/form-line-ledger-select.svelte @@ -0,0 +1,86 @@ +<script lang="ts"> + import { + type ElementCallbackValueKeydown, + type IIdOpt, + type ISelectCallback, + type ISelectOption, + fmt_id, + } from "@radroots/apps-lib"; + import InputPwa from "../lib/input-pwa.svelte"; + import SelectPwa from "../lib/select-pwa.svelte"; + + let { + basis, + value_input = $bindable(``), + value_sel = $bindable(``), + }: { + basis: IIdOpt & { + display_value?: string; + label?: string; + input: { + placeholder?: string; + callback_keydown?: + | ElementCallbackValueKeydown<HTMLInputElement> + | undefined; + }; + select: { + entries: ISelectOption<string>[]; + callback?: ISelectCallback; + }; + }; + value_input?: string; + value_sel?: string; + } = $props(); + + const id = $derived(basis.id || ``); +</script> + +<div class={`flex flex-col w-full gap-2 justify-start items-start`}> + {#if basis.label} + <div class={`flex flex-row w-full justify-start items-center`}> + <p class={`font-sansd text-trellis_ti text-ly0-gl-label uppercase`}> + {basis.label} + </p> + </div> + {/if} + <div + class={`relative flex flex-row h-12 w-full justify-start items-center border-y-line border-ly0-edge`} + > + {#if basis.display_value} + <p class={`font-sans font-[400] text-ly0-gl text-form_base`}> + {basis.display_value} + </p> + {:else} + <InputPwa + bind:value={value_input} + basis={{ + id: id ? fmt_id(`${id}_input`) : undefined, + layer: 0, + classes: `h-10 placeholder:text-[1.1rem]`, + placeholder: basis.input.placeholder || ``, + callback_keydown: basis.input.callback_keydown, + }} + /> + <div + class={`absolute right-0 flex flex-row justify-center items-center`} + > + <SelectPwa + bind:value={value_sel} + basis={{ + classes: `w-fit text-ly1-gl`, + id: id ? fmt_id(`${id}_sel`) : undefined, + sync: true, + layer: 1, + show_arrows: `r`, + options: [ + { + entries: basis.select.entries, + }, + ], + callback: basis.select.callback, + }} + /> + </div> + {/if} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/forms/form-line-ledger.svelte b/apps-lib-pwa/src/lib/components/forms/form-line-ledger.svelte @@ -0,0 +1,88 @@ +<script lang="ts"> + import { + type ElementCallbackValueKeydown, + type IIdOpt, + type ISelectOption, + fmt_id, + } from "@radroots/apps-lib"; + import { type FormField } from "@radroots/utils"; + import InputPwa from "../lib/input-pwa.svelte"; + import SelectMenu from "../lib/select-menu.svelte"; + import FormLineLedgerLabelSelectLabel from "./form-line-ledger-label-select-label.svelte"; + + let { + basis, + value = $bindable(``), + value_label_sel = $bindable(``), + }: { + basis: IIdOpt & { + display_value?: string; + label?: string; + label_select?: { + label: string; + entries: ISelectOption<string>[]; + }; + input?: { + placeholder?: string; + field?: FormField; + callback_keydown?: + | ElementCallbackValueKeydown<HTMLInputElement> + | undefined; + }; + }; + value?: string; + value_label_sel?: string; + } = $props(); + + const id = $derived(basis.id || ``); +</script> + +<div class={`flex flex-col w-full gap-2 justify-start items-start`}> + {#if basis.label} + <div class={`flex flex-row w-full justify-start gap-1 items-center`}> + <p class={`font-sansd text-trellis_ti text-ly0-gl-label uppercase`}> + {basis.label} + </p> + {#if basis.label_select} + <SelectMenu + bind:value={value_label_sel} + basis={{ + layer: 0, + options: [ + { + entries: basis.label_select.entries, + }, + ], + }} + > + <FormLineLedgerLabelSelectLabel + basis={{ + label: basis.label_select.label, + }} + /> + </SelectMenu> + {/if} + </div> + {/if} + <div + class={`flex flex-row h-12 w-full justify-start items-center border-y-line border-ly0-edge`} + > + {#if basis.display_value} + <p class={`font-sans font-[400] text-ly1-gl text-form_base`}> + {basis.display_value} + </p> + {:else if basis.input} + <InputPwa + bind:value + basis={{ + id: id ? fmt_id(id) : undefined, + layer: 0, + classes: `h-10 placeholder:text-[1.1rem]`, + field: basis.input?.field || undefined, + placeholder: basis.input?.placeholder || ``, + callback_keydown: basis.input?.callback_keydown, + }} + /> + {/if} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/layouts/layout-bottom-button.svelte b/apps-lib-pwa/src/lib/components/layouts/layout-bottom-button.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import { app_lo } from "$lib/stores/app"; + import type { Snippet } from "svelte"; + + let { + basis, + children, + }: { + basis?: { + hidden: boolean; + }; + children: Snippet; + } = $props(); +</script> + +{#if !basis?.hidden} + <div + class={`z-10 absolute bottom-0 h-lo_bottom_button_${$app_lo} flex flex-col w-full px-4 gap-1 justify-start items-center`} + > + {@render children()} + </div> +{/if} diff --git a/apps-lib-pwa/src/lib/components/layouts/layout-page.svelte b/apps-lib-pwa/src/lib/components/layouts/layout-page.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import { type IBasisOpt, type IClOpt, fmt_cl } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis = undefined, + children, + }: { + basis?: IBasisOpt<IClOpt>; + children: Snippet; + } = $props(); +</script> + +<div + class={`${fmt_cl(basis?.classes)} flex flex-col w-full pt-4 px-4 pb-24 gap-4 justify-center items-center`} +> + {@render children()} +</div> diff --git a/apps-lib-pwa/src/lib/components/layouts/layout-trellis.svelte b/apps-lib-pwa/src/lib/components/layouts/layout-trellis.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import { type IBasisOpt, type IClOpt, fmt_cl } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis = undefined, + children, + }: { + basis?: IBasisOpt<IClOpt>; + children: Snippet; + } = $props(); +</script> + +<div + class={`${fmt_cl(basis?.classes)} flex flex-col pb-12 gap-6 justify-center items-center scroll-hide`} +> + {@render children()} +</div> diff --git a/apps-lib-pwa/src/lib/components/layouts/layout-view.svelte b/apps-lib-pwa/src/lib/components/layouts/layout-view.svelte @@ -0,0 +1,48 @@ +<script lang="ts"> + import { nav_blur, ph_blur, tabs_blur } from "$lib/stores/app"; + import { fmt_cl, type IBasisOpt, type IClOpt } from "@radroots/apps-lib"; + import { onDestroy, onMount, type Snippet } from "svelte"; + + let { + basis = undefined, + el = $bindable(null), + children, + }: { + el?: HTMLDivElement | null; + basis?: IBasisOpt<IClOpt & { fade?: boolean }>; + children: Snippet; + } = $props(); + + onMount(async () => { + try { + el?.addEventListener("scroll", scrollChange); + } catch (e) { + } finally { + } + }); + + onDestroy(async () => { + try { + el?.removeEventListener("scroll", scrollChange); + } catch (e) { + } finally { + } + }); + + const scrollChange = (): void => { + if (Math.max(el?.scrollTop || 0, 0) > 10) nav_blur.set(true); + else nav_blur.set(false); + if (Math.max(el?.scrollTop || 0, 0) > 10) tabs_blur.set(true); + else tabs_blur.set(false); + if (Math.max(el?.scrollTop || 0, 0) > 30) ph_blur.set(true); + else ph_blur.set(false); + }; +</script> + +<div + bind:this={el} + class={`${fmt_cl(basis?.classes)} absolute top-0 left-0 flex flex-col h-[100vh] w-full justify-start items-center scroll-hide overflow-auto`} + class:fade-in={basis?.fade} +> + {@render children()} +</div> diff --git a/apps-lib-pwa/src/lib/components/layouts/layout-window.svelte b/apps-lib-pwa/src/lib/components/layouts/layout-window.svelte @@ -0,0 +1,35 @@ +<script lang="ts"> + import { + app_lo, + app_tilt, + envelope_tilt, + envelope_visible, + } from "$lib/stores/app"; + import { handle_err, window_set } from "@radroots/apps-lib"; + import { onMount } from "svelte"; + + let { children } = $props(); + + onMount(async () => { + try { + window_set(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + envelope_visible.subscribe(async (_envelope_visible) => { + if (_envelope_visible && $envelope_tilt) app_tilt.set(true); + else app_tilt.set(false); + }); +</script> + +<div + class={`relative lg:hidden flex flex-col h-[100vh] w-full bg-ly0 ${$app_tilt ? `scale-y-[96%] translate-y-4 rounded-t-[3rem]` : ``} overflow-x-hidden overflow-y-scroll scroll-hide delay-75 duration-200 el-re`} +> + {#if $app_lo} + <div class={`flex flex-col h-full w-full`}> + {@render children()} + </div> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/css.svelte b/apps-lib-pwa/src/lib/components/lib/css.svelte @@ -0,0 +1,3 @@ +<div + class="hidden -bottom-lo_bottom_button_ios0 -bottom-lo_bottom_button_ios1 -bottom-lo_bottom_button_webm0 -bottom-lo_bottom_button_webm1 -bottom-lo_view_main_ios0 -bottom-lo_view_main_ios1 -bottom-lo_view_main_webm0 -bottom-lo_view_main_webm1 -bottom-nav_page_header_ios0 -bottom-nav_page_header_ios1 -bottom-nav_page_header_webm0 -bottom-nav_page_header_webm1 -bottom-nav_page_toolbar_ios0 -bottom-nav_page_toolbar_ios1 -bottom-nav_page_toolbar_webm0 -bottom-nav_page_toolbar_webm1 -bottom-nav_tabs_ios0 -bottom-nav_tabs_ios1 -bottom-nav_tabs_webm0 -bottom-nav_tabs_webm1 -left-lo_ios0 -left-lo_ios1 -left-lo_line_entry_ios0 -left-lo_line_entry_ios1 -left-lo_line_entry_webm0 -left-lo_line_entry_webm1 -left-lo_textdesc_ios0 -left-lo_textdesc_ios1 -left-lo_textdesc_webm0 -left-lo_textdesc_webm1 -left-lo_webm0 -left-lo_webm1 -right-lo_ios0 -right-lo_ios1 -right-lo_line_entry_ios0 -right-lo_line_entry_ios1 -right-lo_line_entry_webm0 -right-lo_line_entry_webm1 -right-lo_textdesc_ios0 -right-lo_textdesc_ios1 -right-lo_textdesc_webm0 -right-lo_textdesc_webm1 -right-lo_webm0 -right-lo_webm1 -top-lo_bottom_button_ios0 -top-lo_bottom_button_ios1 -top-lo_bottom_button_webm0 -top-lo_bottom_button_webm1 -top-lo_view_main_ios0 -top-lo_view_main_ios1 -top-lo_view_main_webm0 -top-lo_view_main_webm1 -top-nav_page_header_ios0 -top-nav_page_header_ios1 -top-nav_page_header_webm0 -top-nav_page_header_webm1 -top-nav_page_toolbar_ios0 -top-nav_page_toolbar_ios1 -top-nav_page_toolbar_webm0 -top-nav_page_toolbar_webm1 -top-nav_tabs_ios0 -top-nav_tabs_ios1 -top-nav_tabs_webm0 -top-nav_tabs_webm1 -translate-x-w_lo_ios0 -translate-x-w_lo_ios1 -translate-x-w_lo_line_entry_ios0 -translate-x-w_lo_line_entry_ios1 -translate-x-w_lo_line_entry_webm0 -translate-x-w_lo_line_entry_webm1 -translate-x-w_lo_textdesc_ios0 -translate-x-w_lo_textdesc_ios1 -translate-x-w_lo_textdesc_webm0 -translate-x-w_lo_textdesc_webm1 -translate-x-w_lo_webm0 -translate-x-w_lo_webm1 -translate-y-h_lo_bottom_button_ios0 -translate-y-h_lo_bottom_button_ios1 -translate-y-h_lo_bottom_button_webm0 -translate-y-h_lo_bottom_button_webm1 -translate-y-h_lo_view_main_ios0 -translate-y-h_lo_view_main_ios1 -translate-y-h_lo_view_main_webm0 -translate-y-h_lo_view_main_webm1 -translate-y-h_nav_page_header_ios0 -translate-y-h_nav_page_header_ios1 -translate-y-h_nav_page_header_webm0 -translate-y-h_nav_page_header_webm1 -translate-y-h_nav_page_toolbar_ios0 -translate-y-h_nav_page_toolbar_ios1 -translate-y-h_nav_page_toolbar_webm0 -translate-y-h_nav_page_toolbar_webm1 -translate-y-h_nav_tabs_ios0 -translate-y-h_nav_tabs_ios1 -translate-y-h_nav_tabs_webm0 -translate-y-h_nav_tabs_webm1 active:bg-ly0-gl active:bg-ly0-gl-hl active:bg-ly0-gl-hl_a active:bg-ly0-gl-label active:bg-ly0-gl-shade active:bg-ly0-gl-a active:bg-ly0-gl-pl active:bg-ly0 active:bg-ly0-blur active:bg-ly0-edge active:bg-ly0-a active:bg-ly0-w active:bg-ly1-gl active:bg-ly1-gl-hl active:bg-ly1-gl-hl_a active:bg-ly1-gl-label active:bg-ly1-gl-shade active:bg-ly1-gl-a active:bg-ly1-gl-d active:bg-ly1-gl-pl active:bg-ly1 active:bg-ly1-edge active:bg-ly1-err active:bg-ly1-focus active:bg-ly1-a active:bg-ly2-gl active:bg-ly2-gl-hl active:bg-ly2-gl-hl_a active:bg-ly2-gl-shade active:bg-ly2-gl-a active:bg-ly2-gl-d active:bg-ly2-gl-pl active:bg-ly2 active:bg-ly2-edge active:bg-ly2-a active:bg-radroots-accent-focus active:border-ly0-gl active:border-ly0-gl-hl active:border-ly0-gl-hl_a active:border-ly0-gl-label active:border-ly0-gl-shade active:border-ly0-gl-a active:border-ly0-gl-pl active:border-ly0 active:border-ly0-blur active:border-ly0-edge active:border-ly0-a active:border-ly0-w active:border-ly1-gl active:border-ly1-gl-hl active:border-ly1-gl-hl_a active:border-ly1-gl-label active:border-ly1-gl-shade active:border-ly1-gl-a active:border-ly1-gl-d active:border-ly1-gl-pl active:border-ly1 active:border-ly1-edge active:border-ly1-err active:border-ly1-focus active:border-ly1-a active:border-ly2-gl active:border-ly2-gl-hl active:border-ly2-gl-hl_a active:border-ly2-gl-shade active:border-ly2-gl-a active:border-ly2-gl-d active:border-ly2-gl-pl active:border-ly2 active:border-ly2-edge active:border-ly2-a active:border-radroots-accent-focus active:text-ly0-gl active:text-ly0-gl-hl active:text-ly0-gl-hl_a active:text-ly0-gl-label active:text-ly0-gl-shade active:text-ly0-gl-a active:text-ly0-gl-pl active:text-ly0 active:text-ly0-blur active:text-ly0-edge active:text-ly0-a active:text-ly0-w active:text-ly1-gl active:text-ly1-gl-hl active:text-ly1-gl-hl_a active:text-ly1-gl-label active:text-ly1-gl-shade active:text-ly1-gl-a active:text-ly1-gl-d active:text-ly1-gl-pl active:text-ly1 active:text-ly1-edge active:text-ly1-err active:text-ly1-focus active:text-ly1-a active:text-ly2-gl active:text-ly2-gl-hl active:text-ly2-gl-hl_a active:text-ly2-gl-shade active:text-ly2-gl-a active:text-ly2-gl-d active:text-ly2-gl-pl active:text-ly2 active:text-ly2-edge active:text-ly2-a active:text-radroots-accent-focus bg-ly0-gl bg-ly0-gl-hl bg-ly0-gl-hl_a bg-ly0-gl-label bg-ly0-gl-shade bg-ly0-gl-a bg-ly0-gl-pl bg-ly0 bg-ly0-blur bg-ly0-edge bg-ly0-a bg-ly0-w bg-ly1-gl bg-ly1-gl-hl bg-ly1-gl-hl_a bg-ly1-gl-label bg-ly1-gl-shade bg-ly1-gl-a bg-ly1-gl-d bg-ly1-gl-pl bg-ly1 bg-ly1-edge bg-ly1-err bg-ly1-focus bg-ly1-a bg-ly2-gl bg-ly2-gl-hl bg-ly2-gl-hl_a bg-ly2-gl-shade bg-ly2-gl-a bg-ly2-gl-d bg-ly2-gl-pl bg-ly2 bg-ly2-edge bg-ly2-a bg-radroots-accent-focus border-ly0-gl border-ly0-gl-hl border-ly0-gl-hl_a border-ly0-gl-label border-ly0-gl-shade border-ly0-gl-a border-ly0-gl-pl border-ly0 border-ly0-blur border-ly0-edge border-ly0-a border-ly0-w border-ly1-gl border-ly1-gl-hl border-ly1-gl-hl_a border-ly1-gl-label border-ly1-gl-shade border-ly1-gl-a border-ly1-gl-d border-ly1-gl-pl border-ly1 border-ly1-edge border-ly1-err border-ly1-focus border-ly1-a border-ly2-gl border-ly2-gl-hl border-ly2-gl-hl_a border-ly2-gl-shade border-ly2-gl-a border-ly2-gl-d border-ly2-gl-pl border-ly2 border-ly2-edge border-ly2-a border-radroots-accent-focus bottom-lo_bottom_button_ios0 bottom-lo_bottom_button_ios1 bottom-lo_bottom_button_webm0 bottom-lo_bottom_button_webm1 bottom-lo_view_main_ios0 bottom-lo_view_main_ios1 bottom-lo_view_main_webm0 bottom-lo_view_main_webm1 bottom-nav_page_header_ios0 bottom-nav_page_header_ios1 bottom-nav_page_header_webm0 bottom-nav_page_header_webm1 bottom-nav_page_toolbar_ios0 bottom-nav_page_toolbar_ios1 bottom-nav_page_toolbar_webm0 bottom-nav_page_toolbar_webm1 bottom-nav_tabs_ios0 bottom-nav_tabs_ios1 bottom-nav_tabs_webm0 bottom-nav_tabs_webm1 focus:bg-ly0-gl focus:bg-ly0-gl-hl focus:bg-ly0-gl-hl_a focus:bg-ly0-gl-label focus:bg-ly0-gl-shade focus:bg-ly0-gl-a focus:bg-ly0-gl-pl focus:bg-ly0 focus:bg-ly0-blur focus:bg-ly0-edge focus:bg-ly0-a focus:bg-ly0-w focus:bg-ly1-gl focus:bg-ly1-gl-hl focus:bg-ly1-gl-hl_a focus:bg-ly1-gl-label focus:bg-ly1-gl-shade focus:bg-ly1-gl-a focus:bg-ly1-gl-d focus:bg-ly1-gl-pl focus:bg-ly1 focus:bg-ly1-edge focus:bg-ly1-err focus:bg-ly1-focus focus:bg-ly1-a focus:bg-ly2-gl focus:bg-ly2-gl-hl focus:bg-ly2-gl-hl_a focus:bg-ly2-gl-shade focus:bg-ly2-gl-a focus:bg-ly2-gl-d focus:bg-ly2-gl-pl focus:bg-ly2 focus:bg-ly2-edge focus:bg-ly2-a focus:bg-radroots-accent-focus focus:border-ly0-gl focus:border-ly0-gl-hl focus:border-ly0-gl-hl_a focus:border-ly0-gl-label focus:border-ly0-gl-shade focus:border-ly0-gl-a focus:border-ly0-gl-pl focus:border-ly0 focus:border-ly0-blur focus:border-ly0-edge focus:border-ly0-a focus:border-ly0-w focus:border-ly1-gl focus:border-ly1-gl-hl focus:border-ly1-gl-hl_a focus:border-ly1-gl-label focus:border-ly1-gl-shade focus:border-ly1-gl-a focus:border-ly1-gl-d focus:border-ly1-gl-pl focus:border-ly1 focus:border-ly1-edge focus:border-ly1-err focus:border-ly1-focus focus:border-ly1-a focus:border-ly2-gl focus:border-ly2-gl-hl focus:border-ly2-gl-hl_a focus:border-ly2-gl-shade focus:border-ly2-gl-a focus:border-ly2-gl-d focus:border-ly2-gl-pl focus:border-ly2 focus:border-ly2-edge focus:border-ly2-a focus:border-radroots-accent-focus focus:text-ly0-gl focus:text-ly0-gl-hl focus:text-ly0-gl-hl_a focus:text-ly0-gl-label focus:text-ly0-gl-shade focus:text-ly0-gl-a focus:text-ly0-gl-pl focus:text-ly0 focus:text-ly0-blur focus:text-ly0-edge focus:text-ly0-a focus:text-ly0-w focus:text-ly1-gl focus:text-ly1-gl-hl focus:text-ly1-gl-hl_a focus:text-ly1-gl-label focus:text-ly1-gl-shade focus:text-ly1-gl-a focus:text-ly1-gl-d focus:text-ly1-gl-pl focus:text-ly1 focus:text-ly1-edge focus:text-ly1-err focus:text-ly1-focus focus:text-ly1-a focus:text-ly2-gl focus:text-ly2-gl-hl focus:text-ly2-gl-hl_a focus:text-ly2-gl-shade focus:text-ly2-gl-a focus:text-ly2-gl-d focus:text-ly2-gl-pl focus:text-ly2 focus:text-ly2-edge focus:text-ly2-a focus:text-radroots-accent-focus group-active:bg-ly0-gl group-active:bg-ly0-gl-hl group-active:bg-ly0-gl-hl_a group-active:bg-ly0-gl-label group-active:bg-ly0-gl-shade group-active:bg-ly0-gl-a group-active:bg-ly0-gl-pl group-active:bg-ly0 group-active:bg-ly0-blur group-active:bg-ly0-edge group-active:bg-ly0-a group-active:bg-ly0-w group-active:bg-ly1-gl group-active:bg-ly1-gl-hl group-active:bg-ly1-gl-hl_a group-active:bg-ly1-gl-label group-active:bg-ly1-gl-shade group-active:bg-ly1-gl-a group-active:bg-ly1-gl-d group-active:bg-ly1-gl-pl group-active:bg-ly1 group-active:bg-ly1-edge group-active:bg-ly1-err group-active:bg-ly1-focus group-active:bg-ly1-a group-active:bg-ly2-gl group-active:bg-ly2-gl-hl group-active:bg-ly2-gl-hl_a group-active:bg-ly2-gl-shade group-active:bg-ly2-gl-a group-active:bg-ly2-gl-d group-active:bg-ly2-gl-pl group-active:bg-ly2 group-active:bg-ly2-edge group-active:bg-ly2-a group-active:bg-radroots-accent-focus group-active:border-ly0-gl group-active:border-ly0-gl-hl group-active:border-ly0-gl-hl_a group-active:border-ly0-gl-label group-active:border-ly0-gl-shade group-active:border-ly0-gl-a group-active:border-ly0-gl-pl group-active:border-ly0 group-active:border-ly0-blur group-active:border-ly0-edge group-active:border-ly0-a group-active:border-ly0-w group-active:border-ly1-gl group-active:border-ly1-gl-hl group-active:border-ly1-gl-hl_a group-active:border-ly1-gl-label group-active:border-ly1-gl-shade group-active:border-ly1-gl-a group-active:border-ly1-gl-d group-active:border-ly1-gl-pl group-active:border-ly1 group-active:border-ly1-edge group-active:border-ly1-err group-active:border-ly1-focus group-active:border-ly1-a group-active:border-ly2-gl group-active:border-ly2-gl-hl group-active:border-ly2-gl-hl_a group-active:border-ly2-gl-shade group-active:border-ly2-gl-a group-active:border-ly2-gl-d group-active:border-ly2-gl-pl group-active:border-ly2 group-active:border-ly2-edge group-active:border-ly2-a group-active:border-radroots-accent-focus group-active:text-ly0-gl group-active:text-ly0-gl-hl group-active:text-ly0-gl-hl_a group-active:text-ly0-gl-label group-active:text-ly0-gl-shade group-active:text-ly0-gl-a group-active:text-ly0-gl-pl group-active:text-ly0 group-active:text-ly0-blur group-active:text-ly0-edge group-active:text-ly0-a group-active:text-ly0-w group-active:text-ly1-gl group-active:text-ly1-gl-hl group-active:text-ly1-gl-hl_a group-active:text-ly1-gl-label group-active:text-ly1-gl-shade group-active:text-ly1-gl-a group-active:text-ly1-gl-d group-active:text-ly1-gl-pl group-active:text-ly1 group-active:text-ly1-edge group-active:text-ly1-err group-active:text-ly1-focus group-active:text-ly1-a group-active:text-ly2-gl group-active:text-ly2-gl-hl group-active:text-ly2-gl-hl_a group-active:text-ly2-gl-shade group-active:text-ly2-gl-a group-active:text-ly2-gl-d group-active:text-ly2-gl-pl group-active:text-ly2 group-active:text-ly2-edge group-active:text-ly2-a group-active:text-radroots-accent-focus group-focus:bg-ly0-gl group-focus:bg-ly0-gl-hl group-focus:bg-ly0-gl-hl_a group-focus:bg-ly0-gl-label group-focus:bg-ly0-gl-shade group-focus:bg-ly0-gl-a group-focus:bg-ly0-gl-pl group-focus:bg-ly0 group-focus:bg-ly0-blur group-focus:bg-ly0-edge group-focus:bg-ly0-a group-focus:bg-ly0-w group-focus:bg-ly1-gl group-focus:bg-ly1-gl-hl group-focus:bg-ly1-gl-hl_a group-focus:bg-ly1-gl-label group-focus:bg-ly1-gl-shade group-focus:bg-ly1-gl-a group-focus:bg-ly1-gl-d group-focus:bg-ly1-gl-pl group-focus:bg-ly1 group-focus:bg-ly1-edge group-focus:bg-ly1-err group-focus:bg-ly1-focus group-focus:bg-ly1-a group-focus:bg-ly2-gl group-focus:bg-ly2-gl-hl group-focus:bg-ly2-gl-hl_a group-focus:bg-ly2-gl-shade group-focus:bg-ly2-gl-a group-focus:bg-ly2-gl-d group-focus:bg-ly2-gl-pl group-focus:bg-ly2 group-focus:bg-ly2-edge group-focus:bg-ly2-a group-focus:bg-radroots-accent-focus group-focus:border-ly0-gl group-focus:border-ly0-gl-hl group-focus:border-ly0-gl-hl_a group-focus:border-ly0-gl-label group-focus:border-ly0-gl-shade group-focus:border-ly0-gl-a group-focus:border-ly0-gl-pl group-focus:border-ly0 group-focus:border-ly0-blur group-focus:border-ly0-edge group-focus:border-ly0-a group-focus:border-ly0-w group-focus:border-ly1-gl group-focus:border-ly1-gl-hl group-focus:border-ly1-gl-hl_a group-focus:border-ly1-gl-label group-focus:border-ly1-gl-shade group-focus:border-ly1-gl-a group-focus:border-ly1-gl-d group-focus:border-ly1-gl-pl group-focus:border-ly1 group-focus:border-ly1-edge group-focus:border-ly1-err group-focus:border-ly1-focus group-focus:border-ly1-a group-focus:border-ly2-gl group-focus:border-ly2-gl-hl group-focus:border-ly2-gl-hl_a group-focus:border-ly2-gl-shade group-focus:border-ly2-gl-a group-focus:border-ly2-gl-d group-focus:border-ly2-gl-pl group-focus:border-ly2 group-focus:border-ly2-edge group-focus:border-ly2-a group-focus:border-radroots-accent-focus group-focus:text-ly0-gl group-focus:text-ly0-gl-hl group-focus:text-ly0-gl-hl_a group-focus:text-ly0-gl-label group-focus:text-ly0-gl-shade group-focus:text-ly0-gl-a group-focus:text-ly0-gl-pl group-focus:text-ly0 group-focus:text-ly0-blur group-focus:text-ly0-edge group-focus:text-ly0-a group-focus:text-ly0-w group-focus:text-ly1-gl group-focus:text-ly1-gl-hl group-focus:text-ly1-gl-hl_a group-focus:text-ly1-gl-label group-focus:text-ly1-gl-shade group-focus:text-ly1-gl-a group-focus:text-ly1-gl-d group-focus:text-ly1-gl-pl group-focus:text-ly1 group-focus:text-ly1-edge group-focus:text-ly1-err group-focus:text-ly1-focus group-focus:text-ly1-a group-focus:text-ly2-gl group-focus:text-ly2-gl-hl group-focus:text-ly2-gl-hl_a group-focus:text-ly2-gl-shade group-focus:text-ly2-gl-a group-focus:text-ly2-gl-d group-focus:text-ly2-gl-pl group-focus:text-ly2 group-focus:text-ly2-edge group-focus:text-ly2-a group-focus:text-radroots-accent-focus h-[12px] h-[16px] h-[17px] h-[18px] h-[20px] h-[22px] h-[24px] h-[28px] h-[36px] h-lo_bottom_button_ios0 h-lo_bottom_button_ios1 h-lo_bottom_button_webm0 h-lo_bottom_button_webm1 h-lo_view_main_ios0 h-lo_view_main_ios1 h-lo_view_main_webm0 h-lo_view_main_webm1 h-nav_page_header_ios0 h-nav_page_header_ios1 h-nav_page_header_webm0 h-nav_page_header_webm1 h-nav_page_toolbar_ios0 h-nav_page_toolbar_ios1 h-nav_page_toolbar_webm0 h-nav_page_toolbar_webm1 h-nav_tabs_ios0 h-nav_tabs_ios1 h-nav_tabs_webm0 h-nav_tabs_webm1 left-lo_ios0 left-lo_ios1 left-lo_line_entry_ios0 left-lo_line_entry_ios1 left-lo_line_entry_webm0 left-lo_line_entry_webm1 left-lo_textdesc_ios0 left-lo_textdesc_ios1 left-lo_textdesc_webm0 left-lo_textdesc_webm1 left-lo_webm0 left-lo_webm1 max-h-lo_bottom_button_ios0 max-h-lo_bottom_button_ios1 max-h-lo_bottom_button_webm0 max-h-lo_bottom_button_webm1 max-h-lo_view_main_ios0 max-h-lo_view_main_ios1 max-h-lo_view_main_webm0 max-h-lo_view_main_webm1 max-h-nav_page_header_ios0 max-h-nav_page_header_ios1 max-h-nav_page_header_webm0 max-h-nav_page_header_webm1 max-h-nav_page_toolbar_ios0 max-h-nav_page_toolbar_ios1 max-h-nav_page_toolbar_webm0 max-h-nav_page_toolbar_webm1 max-h-nav_tabs_ios0 max-h-nav_tabs_ios1 max-h-nav_tabs_webm0 max-h-nav_tabs_webm1 max-w-lo_ios0 max-w-lo_ios1 max-w-lo_line_entry_ios0 max-w-lo_line_entry_ios1 max-w-lo_line_entry_webm0 max-w-lo_line_entry_webm1 max-w-lo_textdesc_ios0 max-w-lo_textdesc_ios1 max-w-lo_textdesc_webm0 max-w-lo_textdesc_webm1 max-w-lo_webm0 max-w-lo_webm1 min-h-lo_bottom_button_ios0 min-h-lo_bottom_button_ios1 min-h-lo_bottom_button_webm0 min-h-lo_bottom_button_webm1 min-h-lo_view_main_ios0 min-h-lo_view_main_ios1 min-h-lo_view_main_webm0 min-h-lo_view_main_webm1 min-h-nav_page_header_ios0 min-h-nav_page_header_ios1 min-h-nav_page_header_webm0 min-h-nav_page_header_webm1 min-h-nav_page_toolbar_ios0 min-h-nav_page_toolbar_ios1 min-h-nav_page_toolbar_webm0 min-h-nav_page_toolbar_webm1 min-h-nav_tabs_ios0 min-h-nav_tabs_ios1 min-h-nav_tabs_webm0 min-h-nav_tabs_webm1 min-w-lo_ios0 min-w-lo_ios1 min-w-lo_line_entry_ios0 min-w-lo_line_entry_ios1 min-w-lo_line_entry_webm0 min-w-lo_line_entry_webm1 min-w-lo_textdesc_ios0 min-w-lo_textdesc_ios1 min-w-lo_textdesc_webm0 min-w-lo_textdesc_webm1 min-w-lo_webm0 min-w-lo_webm1 pb-h_lo_bottom_button_ios0 pb-h_lo_bottom_button_ios1 pb-h_lo_bottom_button_webm0 pb-h_lo_bottom_button_webm1 pb-h_lo_view_main_ios0 pb-h_lo_view_main_ios1 pb-h_lo_view_main_webm0 pb-h_lo_view_main_webm1 pb-h_nav_page_header_ios0 pb-h_nav_page_header_ios1 pb-h_nav_page_header_webm0 pb-h_nav_page_header_webm1 pb-h_nav_page_toolbar_ios0 pb-h_nav_page_toolbar_ios1 pb-h_nav_page_toolbar_webm0 pb-h_nav_page_toolbar_webm1 pb-h_nav_tabs_ios0 pb-h_nav_tabs_ios1 pb-h_nav_tabs_webm0 pb-h_nav_tabs_webm1 pl-w_lo_ios0 pl-w_lo_ios1 pl-w_lo_line_entry_ios0 pl-w_lo_line_entry_ios1 pl-w_lo_line_entry_webm0 pl-w_lo_line_entry_webm1 pl-w_lo_textdesc_ios0 pl-w_lo_textdesc_ios1 pl-w_lo_textdesc_webm0 pl-w_lo_textdesc_webm1 pl-w_lo_webm0 pl-w_lo_webm1 pr-w_lo_ios0 pr-w_lo_ios1 pr-w_lo_line_entry_ios0 pr-w_lo_line_entry_ios1 pr-w_lo_line_entry_webm0 pr-w_lo_line_entry_webm1 pr-w_lo_textdesc_ios0 pr-w_lo_textdesc_ios1 pr-w_lo_textdesc_webm0 pr-w_lo_textdesc_webm1 pr-w_lo_webm0 pr-w_lo_webm1 pt-h_lo_bottom_button_ios0 pt-h_lo_bottom_button_ios1 pt-h_lo_bottom_button_webm0 pt-h_lo_bottom_button_webm1 pt-h_lo_view_main_ios0 pt-h_lo_view_main_ios1 pt-h_lo_view_main_webm0 pt-h_lo_view_main_webm1 pt-h_nav_page_header_ios0 pt-h_nav_page_header_ios1 pt-h_nav_page_header_webm0 pt-h_nav_page_header_webm1 pt-h_nav_page_toolbar_ios0 pt-h_nav_page_toolbar_ios1 pt-h_nav_page_toolbar_webm0 pt-h_nav_page_toolbar_webm1 pt-h_nav_tabs_ios0 pt-h_nav_tabs_ios1 pt-h_nav_tabs_webm0 pt-h_nav_tabs_webm1 right-lo_ios0 right-lo_ios1 right-lo_line_entry_ios0 right-lo_line_entry_ios1 right-lo_line_entry_webm0 right-lo_line_entry_webm1 right-lo_textdesc_ios0 right-lo_textdesc_ios1 right-lo_textdesc_webm0 right-lo_textdesc_webm1 right-lo_webm0 right-lo_webm1 text-[12px] text-[15px] text-[16px] text-[18px] text-[19px] text-[20px] text-[21px] text-[23px] text-[24px] text-[26px] text-[27px] text-[28px] text-[30px] text-[36px] text-[40px] text-ly0-gl text-ly0-gl-hl text-ly0-gl-hl_a text-ly0-gl-label text-ly0-gl-shade text-ly0-gl-a text-ly0-gl-pl text-ly0 text-ly0-blur text-ly0-edge text-ly0-a text-ly0-w text-ly1-gl text-ly1-gl-hl text-ly1-gl-hl_a text-ly1-gl-label text-ly1-gl-shade text-ly1-gl-a text-ly1-gl-d text-ly1-gl-pl text-ly1 text-ly1-edge text-ly1-err text-ly1-focus text-ly1-a text-ly2-gl text-ly2-gl-hl text-ly2-gl-hl_a text-ly2-gl-shade text-ly2-gl-a text-ly2-gl-d text-ly2-gl-pl text-ly2 text-ly2-edge text-ly2-a text-radroots-accent-focus top-lo_bottom_button_ios0 top-lo_bottom_button_ios1 top-lo_bottom_button_webm0 top-lo_bottom_button_webm1 top-lo_view_main_ios0 top-lo_view_main_ios1 top-lo_view_main_webm0 top-lo_view_main_webm1 top-nav_page_header_ios0 top-nav_page_header_ios1 top-nav_page_header_webm0 top-nav_page_header_webm1 top-nav_page_toolbar_ios0 top-nav_page_toolbar_ios1 top-nav_page_toolbar_webm0 top-nav_page_toolbar_webm1 top-nav_tabs_ios0 top-nav_tabs_ios1 top-nav_tabs_webm0 top-nav_tabs_webm1 translate-x-w_lo_ios0 translate-x-w_lo_ios1 translate-x-w_lo_line_entry_ios0 translate-x-w_lo_line_entry_ios1 translate-x-w_lo_line_entry_webm0 translate-x-w_lo_line_entry_webm1 translate-x-w_lo_textdesc_ios0 translate-x-w_lo_textdesc_ios1 translate-x-w_lo_textdesc_webm0 translate-x-w_lo_textdesc_webm1 translate-x-w_lo_webm0 translate-x-w_lo_webm1 translate-y-h_lo_bottom_button_ios0 translate-y-h_lo_bottom_button_ios1 translate-y-h_lo_bottom_button_webm0 translate-y-h_lo_bottom_button_webm1 translate-y-h_lo_view_main_ios0 translate-y-h_lo_view_main_ios1 translate-y-h_lo_view_main_webm0 translate-y-h_lo_view_main_webm1 translate-y-h_nav_page_header_ios0 translate-y-h_nav_page_header_ios1 translate-y-h_nav_page_header_webm0 translate-y-h_nav_page_header_webm1 translate-y-h_nav_page_toolbar_ios0 translate-y-h_nav_page_toolbar_ios1 translate-y-h_nav_page_toolbar_webm0 translate-y-h_nav_page_toolbar_webm1 translate-y-h_nav_tabs_ios0 translate-y-h_nav_tabs_ios1 translate-y-h_nav_tabs_webm0 translate-y-h_nav_tabs_webm1 w-[12px] w-[16px] w-[17px] w-[18px] w-[20px] w-[22px] w-[24px] w-[28px] w-[36px] w-lo_ios0 w-lo_ios1 w-lo_line_entry_ios0 w-lo_line_entry_ios1 w-lo_line_entry_webm0 w-lo_line_entry_webm1 w-lo_textdesc_ios0 w-lo_textdesc_ios1 w-lo_textdesc_webm0 w-lo_textdesc_webm1 w-lo_webm0 w-lo_webm1" +></div> diff --git a/apps-lib-pwa/src/lib/components/lib/float-page.svelte b/apps-lib-pwa/src/lib/components/lib/float-page.svelte @@ -0,0 +1,12 @@ +<script lang="ts"> + import type { IFloatPage } from "$lib/types/components"; + import type { Snippet } from "svelte"; + + let { basis, children }: { basis: IFloatPage; children: Snippet } = + $props(); +</script> + +<div class={`absolute top-10 ${basis.posx}-6 flex flex-row`}> + {@render children()} +</div> +<div class="hidden left-6 right-6"></div> diff --git a/apps-lib-pwa/src/lib/components/lib/glyph-circle.svelte b/apps-lib-pwa/src/lib/components/lib/glyph-circle.svelte @@ -0,0 +1,21 @@ +<script lang="ts"> + import type { IGlyphCircle } from "$lib/types/components"; + import { fmt_cl, glyph_style_map } from "@radroots/apps-lib"; + import GlyphButton from "../buttons/glyph-button.svelte"; + + let { basis }: { basis: IGlyphCircle } = $props(); + + const styles = $derived( + basis?.glyph?.dim + ? glyph_style_map.get(basis?.glyph?.dim) + : glyph_style_map.get(`sm`), + ); +</script> + +{#if styles?.dim_1} + <div + class={`${fmt_cl(basis?.classes_wrap)} flex flex-col h-[${styles?.dim_1}px] w-[${styles?.dim_1}px] justify-center items-center rounded-full el-re`} + > + <GlyphButton basis={basis?.glyph} /> + </div> +{/if} diff --git a/apps-lib-pwa/src/lib/components/lib/image-upload-add-photo.svelte b/apps-lib-pwa/src/lib/components/lib/image-upload-add-photo.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { get_context, Glyph } from "@radroots/apps-lib"; + import LoadSymbol from "./load-symbol.svelte"; + + const { ls, lc_photos_add } = get_context(`lib`); + + let { + basis, + photo_path = $bindable(``), + }: { + basis: { + loading?: boolean; + }; + photo_path: string; + } = $props(); +</script> + +<div class={`relative flex flex-row w-full justify-center items-center`}> + <button + class={`flex flex-row h-[5rem] w-[5rem] justify-center items-center bg-ly1/60 rounded-full`} + onclick={async () => { + const photo_paths_add = await lc_photos_add(); + if (photo_paths_add) photo_path = photo_paths_add[0]; + }} + > + {#if basis.loading} + <LoadSymbol basis={{ dim: `md` }} /> + {:else} + <Glyph + basis={{ + classes: `text-[40px] text-ly2-gl`, + dim: `sm`, + key: `camera`, + }} + /> + {/if} + + <div + class={`absolute -bottom-[1.8rem] flex flex-row justify-start items-center`} + > + <p class={`font-arch font-[600] text-sm text-ly0-gl capitalize`}> + {`${$ls(`icu.add_*`, { value: `${$ls(`common.photo`)}` })}`} + </p> + </div> + </button> +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/input-pwa.svelte b/apps-lib-pwa/src/lib/components/lib/input-pwa.svelte @@ -0,0 +1,120 @@ +<script lang="ts"> + import { browser } from "$app/environment"; + import { + fmt_cl, + idb, + type IInput, + parse_layer, + value_constrain, + } from "@radroots/apps-lib"; + import { handle_err } from "@radroots/utils"; + import { onMount } from "svelte"; + + let { + basis, + el = $bindable(null), + value = $bindable(``), + }: { + basis: IInput<string>; + el?: HTMLInputElement | null; + value?: string; + } = $props(); + + const id = $derived(basis?.id ? basis.id : null); + const layer = $derived( + typeof basis?.layer === `boolean` ? 0 : parse_layer(basis?.layer), + ); + const classes_layer = $derived( + typeof basis?.layer === `boolean` || typeof basis?.layer === `undefined` + ? `` + : `bg-ly${layer} text-ly${layer}-gl_d placeholder:text-ly${layer}-gl_pl caret-ly${layer}-gl`, + ); + + const sync_from_idb = async (): Promise<void> => { + if (!browser || !idb || !id) return; + try { + const kv_val = await idb.get(id); + if (kv_val !== null && kv_val !== undefined && kv_val !== value) { + value = kv_val; + } else if (kv_val === null || kv_val === undefined) { + value = ``; + await idb.set(id, ``); + } + } catch (e) { + handle_err(e, `sync_from_idb`); + } + }; + + const sync_to_idb = async (): Promise<void> => { + if (!browser || !idb || !id) return; + try { + await idb.set(id, value || ``); + } catch (e) { + handle_err(e, `input_idb_sync`); + } + }; + + onMount(async () => { + await sync_from_idb(); + if (basis?.callback_mount && el) { + try { + await basis.callback_mount({ el }); + } catch (e) { + handle_err(e, `callback_mount`); + } + } + }); + + $effect(() => { + if (id && basis?.sync && browser && idb) { + (async () => { + await sync_to_idb(); + })(); + } + }); + + const handle_on_input = async (): Promise<void> => { + try { + let val_cur = value; + let pass = true; + if (basis?.field) { + val_cur = value_constrain(basis.field?.charset, val_cur); + if (val_cur !== value) { + value = val_cur; + } + pass = basis.field?.validate.test(val_cur); + } + if (basis?.callback) { + await basis.callback({ value: val_cur, pass }); + } + } catch (e) { + handle_err(e, `handle_on_input`); + } + }; +</script> + +<input + bind:this={el} + bind:value + disabled={!!basis.disabled} + oninput={handle_on_input} + onblur={async ({ currentTarget: el }) => { + if (basis.callback_blur) await basis.callback_blur({ el }); + }} + onfocus={async ({ currentTarget: el }) => { + if (id && basis.sync && browser && idb) await sync_from_idb(); + if (basis.callback_focus) await basis.callback_focus({ el }); + }} + onkeydown={async (ev) => { + if (basis?.callback_keydown) + await basis.callback_keydown({ + key: ev.key, + key_s: ev.key === `Enter`, + el: ev.currentTarget, + }); + }} + {id} + type="text" + class={`${fmt_cl(basis?.classes)} el-input ${classes_layer} el-re`} + placeholder={basis?.placeholder || ``} +/> diff --git a/apps-lib-pwa/src/lib/components/lib/input-value.svelte b/apps-lib-pwa/src/lib/components/lib/input-value.svelte @@ -0,0 +1,71 @@ +<script lang="ts"> + import { + fmt_cl, + type IInputValue, + parse_layer, + value_constrain, + } from "@radroots/apps-lib"; + + let { + basis, + el = $bindable(null), + value = $bindable(``), + }: { + basis: IInputValue<string>; + el?: HTMLInputElement | null; + value: string; + } = $props(); + + const id = $derived(basis?.id ? basis.id : null); + const layer = $derived( + typeof basis?.layer === `boolean` + ? parse_layer(0) + : parse_layer(basis?.layer), + ); + + const classes_layer = $derived( + typeof basis?.layer === `boolean` || typeof basis?.layer === `undefined` + ? `` + : `bg-ly${layer} text-ly${layer}-gl placeholder:text-ly${layer}-gl_pl caret-ly${layer}-gl`, + ); + + const handle_on_input = async (): Promise<void> => { + try { + let val_cur = value; + let pass = true; + if (basis?.field) { + val_cur = value_constrain(basis.field.charset, val_cur); + if (val_cur !== value) value = val_cur; + pass = basis.field.validate.test(val_cur); + } + if (basis?.callback) await basis.callback({ value: val_cur, pass }); + } catch (e) { + console.error(`(error) handle_on_input`, e); + } + }; +</script> + +<input + bind:this={el} + bind:value + disabled={!!basis.disabled} + oninput={handle_on_input} + onblur={async ({ currentTarget: el }) => { + if (basis.callback_blur) await basis.callback_blur({ el }); + }} + onfocus={async ({ currentTarget: el }) => { + if (basis.callback_focus) await basis.callback_focus({ el }); + }} + onkeydown={async (ev) => { + if (basis?.callback_keydown) + await basis.callback_keydown({ + key: ev.key, + key_s: ev.key === `Enter`, + el: ev.currentTarget, + }); + }} + {id} + type="text" + class={`${fmt_cl(basis?.classes)} el-input ${classes_layer} el-re`} + placeholder={basis?.placeholder || ``} +/> diff --git a/apps-lib-pwa/src/lib/components/lib/label-swap.svelte b/apps-lib-pwa/src/lib/components/lib/label-swap.svelte @@ -0,0 +1,41 @@ +<script lang="ts"> + import { + fmt_cl, + type ILabelSwap, + type ILyOpt, + parse_layer, + } from "@radroots/apps-lib"; + + let { + basis, + el = $bindable(null), + }: { + basis: ILabelSwap & ILyOpt; + el?: HTMLLabelElement | null; + } = $props(); + + const layer = $derived(parse_layer(basis?.layer ? basis.layer : 1)); +</script> + +<div class={`flex flex-row justify-start items-center`}> + <!-- svelte-ignore a11y_label_has_associated_control --> + <label + bind:this={el} + class={`swap${basis.swap.toggle ? ` swap-active` : ``}`} + > + <div class="swap-on"> + <p + class={`${fmt_cl(basis.swap.on.classes || `text-nav_prev text-ly${layer}-gl-hl group-active:opacity-60`)} font-sans -translate-y-[1px] el-re`} + > + {basis.swap.on.value} + </p> + </div> + <div class="swap-off"> + <p + class={`${fmt_cl(basis.swap.off.classes || `text-nav_prev text-ly${layer}-gl-hl group-active:opacity-60`)} font-sans -translate-y-[1px] el-re`} + > + {basis.swap.off.value} + </p> + </div> + </label> +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/load-symbol.svelte b/apps-lib-pwa/src/lib/components/lib/load-symbol.svelte @@ -0,0 +1,71 @@ +<script lang="ts"> + import { loading_style_map } from "$lib/styles/lib"; + import type { ILoadSymbol } from "@radroots/apps-lib"; + + let { + basis = undefined, + }: { + basis?: ILoadSymbol; + } = $props(); + + const styles = $derived( + basis?.dim + ? loading_style_map.get(basis?.dim) + : loading_style_map.get("sm"), + ); + + const num_blades = $derived(basis?.blades || 8); +</script> + +<div + class={`relative flex flex-row justify-center items-center h-[${styles?.dim_1}px] w-[${styles?.dim_1}px] fade-in el-re`} +> + <div + class={`${num_blades === 12 ? `spinner12 center` : `spinner8 center`} text-[${styles?.gl_2 || styles?.dim_1}px]`} + > + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + {#if num_blades === 12} + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + <div + class={`${num_blades === 12 ? `spinner12-blade` : `spinner8-blade`}`} + ></div> + {/if} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/logo-circle-sm.svelte b/apps-lib-pwa/src/lib/components/lib/logo-circle-sm.svelte @@ -0,0 +1,14 @@ +<div + class={`relative flex flex-row h-12 w-12 justify-center items-center bg-ly2 rounded-full`} +> + <p + class={`font-sans font-[900] text-[1.5rem] text-ly0-gl -tracking-[0.4rem] -translate-x-[2px]`} + > + {"»`,"} + </p> + <p + class={`font-sans font-[900] text-[1.5rem] text-ly0-gl translate-x-[3px]`} + > + {"-"} + </p> +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/logo-circle.svelte b/apps-lib-pwa/src/lib/components/lib/logo-circle.svelte @@ -0,0 +1,18 @@ +<div + class={`relative flex flex-col h-[196px] w-full justify-center items-center`} +> + <div + class={`relative flex flex-row h-36 w-36 justify-center items-center bg-ly2 rounded-full`} + > + <p + class={`font-sans font-[900] text-6xl text-ly0-gl -tracking-[0.4rem] -translate-x-[6px]`} + > + {"»`,"} + </p> + <p + class={`font-sans font-[900] text-6xl text-ly0-gl translate-x-[8px]`} + > + {"-"} + </p> + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/logo-letters.svelte b/apps-lib-pwa/src/lib/components/lib/logo-letters.svelte @@ -0,0 +1,3 @@ +<p class={`font-sansd italic font-[700] text-[1.7rem] text-ly0-gl lowercase`}> + {`radroots`} +</p> diff --git a/apps-lib-pwa/src/lib/components/lib/select-menu.svelte b/apps-lib-pwa/src/lib/components/lib/select-menu.svelte @@ -0,0 +1,79 @@ +<script lang="ts"> + import { fmt_cl, type ISelect, parse_layer } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis, + value = $bindable(``), + el_wrap = $bindable(null), + el_select = $bindable(null), + children, + }: { + basis: ISelect; + value: string; + el_wrap?: HTMLDivElement | null; + el_select?: HTMLSelectElement | null; + children?: Snippet; + } = $props(); + + const layer = $derived( + parse_layer(typeof basis?.layer === `boolean` ? basis.layer : 0), + ); + + const classes_layer = $derived( + typeof basis?.layer === `boolean` ? `` : `text-ly${layer}-gl`, + ); +</script> + +<div + class={`relative flex flex-row h-max w-auto justify-center items-center`} + bind:this={el_wrap} +> + <div + class={`${fmt_cl(basis.classes)} z-20 absolute top-0 left-0 flex flex-row h-full w-full justify-end items-center ${classes_layer}`} + > + <select + class={`select select-ghost h-full w-full bg-transparent focus:border-0 focus:outline-0 text-transparent focus:text-transparent`} + bind:this={el_select} + bind:value + onchange={async (e) => { + const opt = basis.options + .map((i) => i.entries) + .reduce((_, j) => j, []) + .find( + (k: { value: string }) => + k.value === e.currentTarget?.value, + ); + if (basis.callback && opt) await basis.callback(opt); + if (el_select) el_select.value = value; + }} + > + {#each basis.options as opt_g} + {#if opt_g.group} + <optgroup> + {#each opt_g.entries as opt} + <option + label={opt_g.group === true + ? `-`.repeat(21) + : opt_g.group || ``} + > + {opt.label} + </option> + {/each} + </optgroup> + {:else} + {#each opt_g.entries as opt} + <option value={opt.value} disabled={!!opt.disabled}> + {opt.label} + </option> + {/each} + {/if} + {/each} + </select> + </div> + {#if children} + <div class={`z-10 flex flex-row h-full w-full`}> + {@render children()} + </div> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/lib/select-pwa.svelte b/apps-lib-pwa/src/lib/components/lib/select-pwa.svelte @@ -0,0 +1,126 @@ +<script lang="ts"> + import { browser } from "$app/environment"; + import { + Glyph, + type ISelect, + fmt_cl, + idb, + parse_layer, + } from "@radroots/apps-lib"; + import { handle_err } from "@radroots/utils"; + import { onMount } from "svelte"; + + let { + basis, + value = $bindable(``), + el = $bindable(null), + }: { + basis: ISelect; + value: string; + el?: HTMLSelectElement | null; + } = $props(); + + const id = $derived(basis?.id ? basis.id : null); + + const layer = $derived( + typeof basis?.layer === `boolean` + ? parse_layer(0) + : parse_layer(basis.layer), + ); + + const classes_layer = $derived( + typeof basis?.layer === `boolean` + ? `` + : !value + ? `text-ly${layer}-gl/60` + : `text-ly${layer}-gl_d`, + ); + + onMount(async () => { + try { + if (id && basis?.sync_init && browser) { + const sync_val = await idb.get(id); + await idb.set(id, sync_val || ``); + } + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + $effect(() => { + if (browser && id && basis?.sync) { + (async () => { + await idb.set(id, value); + })(); + } + }); + + const handle_on_change = async (el: HTMLSelectElement): Promise<void> => { + try { + const opt = basis.options + .map((i) => i.entries) + .reduce((_, j) => j, []) + .find((k) => k.value === el?.value); + if (el) el.value = value; + if (basis?.sync && id && browser) await idb.set(id, value); + if (basis.callback && opt) await basis.callback(opt); + } catch (e) { + console.log(`(error) handle_on_change `, e); + } + }; +</script> + +{#if basis?.show_arrows === "l"} + <div class={`flex flex-row pr-[2px] justify-center items-center`}> + <Glyph + basis={{ + key: `caret-up-down`, + dim: `xs`, + + classes: `text-ly${layer}-gl translate-y-[1px]`, + }} + /> + </div> +{/if} +<select + bind:this={el} + bind:value + onchange={async ({ currentTarget: el }) => { + handle_on_change(el); + }} + {id} + class={`${fmt_cl(basis.classes)} z-10 el-select ${classes_layer}`} +> + {#each basis.options as opt_g} + {#if opt_g.group} + <optgroup> + {#each opt_g.entries as opt} + <option + label={opt_g.group === true + ? `-`.repeat(21) + : opt_g.group || ``} + > + {opt.label} + </option> + {/each} + </optgroup> + {:else} + {#each opt_g.entries as opt} + <option value={opt.value} disabled={!!opt.disabled}> + {opt.label} + </option> + {/each} + {/if} + {/each} +</select> +{#if basis?.show_arrows === "r"} + <div class={`flex flex-row pl-[2px] justify-center items-center`}> + <Glyph + basis={{ + key: `caret-up-down`, + dim: `xs`, + classes: `text-ly${layer}-gl`, + }} + /> + </div> +{/if} diff --git a/apps-lib-pwa/src/lib/components/lib/wrap-border.svelte b/apps-lib-pwa/src/lib/components/lib/wrap-border.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import { type IClOpt, fmt_cl } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + + let { + basis, + children, + }: { + basis: IClOpt; + children: Snippet; + } = $props(); +</script> + +<div + class={`${fmt_cl(basis.classes)} relative flex flex-col w-full py-[4px] px-[4px] justify-start items-center rounded-[32px] overflow-hidden bg-white shadow-sm`} +> + <div + class={`flex flex-row h-full w-full justify-center items-center bg-white/30 overflow-hidden rounded-[28px]`} + > + {@render children()} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/map/map-marker-area-display.svelte b/apps-lib-pwa/src/lib/components/map/map-marker-area-display.svelte @@ -0,0 +1,57 @@ +<script lang="ts"> + import { Fade, Glyph, type IBasisOpt } from "@radroots/apps-lib"; + + let { + basis = undefined, + }: { + basis?: IBasisOpt<{ + primary: string; + admin: string; + country: string; + }>; + } = $props(); +</script> + +{#if basis} + <Fade + basis={{ + classes: `flex-col w-full justify-center items-start`, + }} + > + <div + class={`flex flex-col w-fit px-5 py-[10px] justify-start items-start bg-ly1 rounded-3xl shadow-lg`} + > + <div class={`flex flex-col w-full gap-1 justify-start items-start`}> + <div class={`flex flex-row gap-1 justify-start items-center`}> + <p + class={`font-sans font-[600] text-[0.95rem] text-ly2-gl`} + > + {basis.primary} + </p> + <Glyph + basis={{ + classes: `text-ly2-gl -translate-y-[2px]`, + dim: `xs`, + + key: `map-pin-simple`, + }} + /> + </div> + <div + class={`flex flex-row w-full gap-1 justify-start items-center`} + > + <p + class={`font-sans font-[600] text-[0.95rem] tracking-tight text-ly2-gl`} + > + {`${basis.admin},`} + </p> + <p + class={`font-sans font-[600] text-[0.95rem] tracking-tight text-ly2-gl`} + > + {`${basis.country}`} + </p> + </div> + </div> + </div> + </Fade> +{/if} diff --git a/apps-lib-pwa/src/lib/components/map/map-marker-area.svelte b/apps-lib-pwa/src/lib/components/map/map-marker-area.svelte @@ -0,0 +1,47 @@ +<script lang="ts"> + import type { IMapMarkerArea } from "$lib/types/components"; + import { get_context } from "@radroots/apps-lib"; + import { + type GeocoderReverseResult, + type GeolocationPoint, + } from "@radroots/utils"; + import { Marker, Popup } from "svelte-maplibre"; + import MapMarkerAreaDisplay from "./map-marker-area-display.svelte"; + + const { lc_geocode } = get_context(`lib`); + + let { + basis, + map_geop = $bindable(), + map_geoc = $bindable(undefined), + }: { + basis: IMapMarkerArea; + map_geop: GeolocationPoint; + map_geoc?: GeocoderReverseResult | undefined; + } = $props(); +</script> + +<Marker + bind:lngLat={map_geop} + draggable={!basis.no_drag} + class={`flex flex-row h-[100px] w-[100px] bg-blue-400/20 border-[2px] border-white justify-center items-center rounded-full shadow-lg`} + ondragend={async () => { + if (!map_geop) return; + const geoc = await lc_geocode(map_geop); + if (geoc) map_geoc = geoc; + }} +> + {#if basis.show_display} + <Popup open={basis.show_display} offset={[0, -55]}> + <MapMarkerAreaDisplay + basis={map_geoc + ? { + primary: map_geoc.name, + admin: map_geoc.admin1_name, + country: map_geoc.country_name, + } + : undefined} + /> + </Popup> + {/if} +</Marker> diff --git a/apps-lib-pwa/src/lib/components/map/map.svelte b/apps-lib-pwa/src/lib/components/map/map.svelte @@ -0,0 +1,42 @@ +<script lang="ts"> + import { cfg_map } from "$lib/utils/map"; + import { type IClOpt, fmt_cl, theme_mode } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + import { MapLibre } from "svelte-maplibre"; + + let { + basis = undefined, + map = $bindable(undefined), + children, + }: { + basis?: IClOpt & { + interactive?: boolean; + zoom_click_off?: boolean; + }; + map?: maplibregl.Map; + interactive?: boolean; + children: Snippet; + } = $props(); + + const interactive = $derived( + typeof basis?.interactive === `boolean` ? basis?.interactive : true, + ); + + const zoomOnDoubleClick = $derived( + typeof basis?.zoom_click_off === `boolean` + ? basis?.zoom_click_off + : true, + ); +</script> + +<MapLibre + bind:map + class="{fmt_cl(basis?.classes)} relative h-full w-full" + zoom={10} + style={cfg_map.styles.base[$theme_mode]} + attributionControl={false} + {interactive} + {zoomOnDoubleClick} +> + {@render children()} +</MapLibre> diff --git a/apps-lib-pwa/src/lib/components/navigation/navigation-tabs.svelte b/apps-lib-pwa/src/lib/components/navigation/navigation-tabs.svelte @@ -0,0 +1,100 @@ +<script lang="ts"> + import { goto } from "$app/navigation"; + import { page } from "$app/state"; + import { app_lo } from "$lib/stores/app"; + import { Flex, Glyph } from "@radroots/apps-lib"; +</script> + +<div + class={`fixed bottom-0 left-0 h-nav_tabs_${$app_lo} flex flex-row w-full pt-2 justify-center items-start`} +> + <div class={`flex flex-row justify-between gap-10 items-center`}> + <div + class={`grid grid-cols-4 flex flex-row h-[3.1rem] px-6 gap-6 justify-start items-center bg-ly1 rounded-full backdrop-blur-lg`} + > + <button + class={`col-span-1 flex flex-row justify-center items-center`} + onclick={async () => { + await goto(`/`); + }} + > + <Glyph + basis={{ + classes: `text-[26px] text-ly0-gl/80 rotate-90`, + key: `columns`, + weight: page.url.pathname === `/` ? `fill` : `bold`, + }} + /> + </button> + <button + class={`relative col-span-1 flex flex-row justify-center items-center`} + onclick={async () => { + await goto(`/search`); + }} + > + <Glyph + basis={{ + classes: `text-[24px] text-ly0-gl/80`, + key: `magnifying-glass`, + weight: page.url.pathname.includes(`search`) + ? `fill` + : `bold`, + }} + /> + </button> + <button + class={`relative col-span-1 flex flex-row justify-center items-center`} + onclick={async () => { + goto(`/profile`); + }} + > + <Glyph + basis={{ + classes: `text-[24px] text-ly0-gl/80`, + key: `user`, + weight: page.url.pathname.includes(`profile`) + ? `fill` + : `bold`, + }} + /> + </button> + <button + class={`relative col-span-1 flex flex-row h-full justify-center items-center`} + onclick={async () => { + await goto(`/notifications`); + }} + > + <Glyph + basis={{ + classes: `text-[24px] text-ly0-gl/80`, + key: `bell`, + weight: page.url.pathname.includes(`notifications`) + ? `fill` + : `bold`, + }} + /> + <div + class={`absolute top-2 -right-1 flex flex-row justify-start items-center`} + > + <div + class={`flex flex-row h-2 w-2 justify-start items-center bg-yellow-400 rounded-full`} + > + <Flex /> + </div> + </div> + </button> + </div> + <button + class={`flex flex-row h-[3.1rem] w-[3.1rem] justify-center items-center bg-ly1 rounded-full backdrop-blur-lg`} + onclick={async () => {}} + > + <Glyph + basis={{ + classes: `text-[22px] text-ly0-gl/80`, + + key: `plus`, + }} + /> + </button> + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/navigation/page-header.svelte b/apps-lib-pwa/src/lib/components/navigation/page-header.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + import { app_lo, ph_blur } from "$lib/stores/app"; + import type { IPageHeader } from "$lib/types/components"; + import { callback_route, Flex } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + import { fade } from "svelte/transition"; + + let { + basis, + children, + }: { + basis: IPageHeader<string>; + children?: Snippet; + } = $props(); +</script> + +{#if $ph_blur} + <div + in:fade={{ duration: 50 }} + out:fade={{ delay: 50, duration: 200 }} + class={`z-20 fixed top-0 left-0 flex flex-row h-nav_page_header_${$app_lo} w-full justify-center items-center bg-ly0-blur/30 backdrop-blur-lg`} + > + <Flex /> + </div> +{/if} +<div + class={`z-20 sticky top-0 flex flex-row min-h-nav_page_header_${$app_lo} h-nav_page_header_${$app_lo} w-full px-6 justify-between items-center`} +> + <div class={`flex flex-row justify-start items-center`}> + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + if (basis.callback_route) + await callback_route(basis.callback_route); + }} + > + <p + class={`font-sansd font-[700] text-2xl text-ly0-gl capitalize max-w-lo_${$app_lo} truncate`} + > + {basis.label || ``} + </p> + </button> + </div> + {#if children} + {#if !$ph_blur} + <div + in:fade={{ duration: 50 }} + out:fade={{ delay: 50, duration: 200 }} + class={`flex flex-row justify-center items-center`} + > + {@render children()} + </div> + {/if} + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/navigation/page-toolbar.svelte b/apps-lib-pwa/src/lib/components/navigation/page-toolbar.svelte @@ -0,0 +1,57 @@ +<script lang="ts"> + import { goto } from "$app/navigation"; + import { app_lo } from "$lib/stores/app"; + import type { IPageToolbar } from "$lib/types/components"; + import { type IBasisOpt, Glyph } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + import LogoCircleSm from "../lib/logo-circle-sm.svelte"; + import LogoLetters from "../lib/logo-letters.svelte"; + import PageHeader from "./page-header.svelte"; + + let { + basis = undefined, + header_option, + }: { + basis?: IBasisOpt<IPageToolbar<string>>; + header_option?: Snippet; + } = $props(); +</script> + +<div + class={`flex flex-row min-h-nav_page_toolbar_${$app_lo} h-nav_page_toolbar_${$app_lo} w-full px-6 justify-between items-end`} +> + <div class={`flex flex-row w-full justify-between items-center`}> + <button + class={`flex flex-row gap-2 justify-start items-center`} + onclick={async () => { + if (basis?.callback) await basis.callback(); + else await goto(`/`); + }} + > + <LogoCircleSm /> + <LogoLetters /> + </button> + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + await goto(`/settings`); + }} + > + <Glyph + basis={{ + classes: `text-ly0-gl`, + dim: `lg`, + + key: `gear`, + }} + /> + </button> + </div> +</div> +{#if basis?.header} + <PageHeader basis={basis.header}> + {#if header_option} + {@render header_option()} + {/if} + </PageHeader> +{/if} diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-default-label.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-default-label.svelte @@ -0,0 +1,37 @@ +<script lang="ts"> + import type { ITrellisDefaultLabel } from "$lib/types/components/trellis"; + import { fmt_cl } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + + let { + layer, + labels, + classes = ``, + }: { + layer: ThemeLayer; + labels: ITrellisDefaultLabel[]; + classes?: string; + } = $props(); +</script> + +<div class={`${fmt_cl(classes)} flex flex-row`}> + <p class={`font-sans text-trellis_ti text-ly${layer}-gl-shade`}> + {#each labels as label} + <span class={`${fmt_cl(label.classes)} font-sans text-trellis_ti`}> + {#if `callback` in label} + <button + class={``} + onclick={async () => { + if (`callback` in label && label.callback) + await label.callback(); + }} + > + {label.label} + </button> + {:else} + {label.label} + {/if} + </span> + {/each} + </p> +</div> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-end.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-end.svelte @@ -0,0 +1,36 @@ +<script lang="ts"> + import type { ITrellisBasisTouchEnd } from "$lib/types/components/trellis"; + import { Glyph } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + + let { + basis, + layer, + hide_active, + }: { + basis: ITrellisBasisTouchEnd; + layer: ThemeLayer; + hide_active: boolean; + } = $props(); +</script> + +<div + class={`absolute top-0 right-0 h-full w-max flex flex-row justify-center items-center`} +> + <button + class={`flex pr-3`} + onclick={async (ev) => { + if (basis.callback) await basis.callback(ev); + }} + > + {#if basis.glyph} + <Glyph + basis={{ + classes: `text-ly${layer}-gl-shade ${hide_active ? `` : `group-active:text-ly${layer}-gl_a`} translate-y-[1px] opacity-70`, + dim: `xs+`, + ...basis.glyph, + }} + /> + {/if} + </button> +</div> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-input.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-input.svelte @@ -0,0 +1,86 @@ +<script lang="ts"> + import type { ITrellisBasisInput } from "$lib/types/components/trellis"; + import { fmt_trellis } from "$lib/utils/app"; + import { fmt_cl, Glyph } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + import InputPwa from "../lib/input-pwa.svelte"; + import LoadSymbol from "../lib/load-symbol.svelte"; + + let { + basis, + layer, + hide_border_b, + hide_border_t, + }: { + basis: ITrellisBasisInput; + layer: ThemeLayer; + hide_border_b: boolean; + hide_border_t: boolean; + } = $props(); +</script> + +<div class={`flex flex-row flex-grow h-full w-full`}> + <div + class={`${fmt_trellis(hide_border_b, hide_border_t)} flex flex-row h-line w-full justify-start items-center border-t-line border-ly${layer}-edge overflow-hidden`} + > + {#if basis.line_label && basis.line_label.value} + <div + class={`${fmt_cl(basis.line_label.classes)} flex flex-row h-full justify-start items-center overflow-x-hidden`} + > + <p class={`font-sans text-ly${layer}-gl_b`}> + {basis.line_label.value} + </p> + </div> + {/if} + <div + class={`relative flex flex-row flex-grow h-full pr-12 justify-start items-center`} + > + <InputPwa + basis={{ + ...basis.basis, + layer: layer, + }} + /> + {#if basis.action} + {#if basis.action.visible} + <div + class={`absolute top-0 right-0 flex flex-row h-full w-12 pr-4 justify-end items-center fade-in`} + > + {#if basis.action.loading} + <div class={`flex flex-row fade-in`}> + <LoadSymbol + basis={{ + dim: `glyph-send-button`, + blades: 8, + classes: `text-ly${layer}-gl el-re`, + }} + /> + </div> + {:else} + <button + class={`group fade-in-long`} + onclick={async () => { + if (basis.action?.callback) + await basis.action.callback(); + }} + > + <Glyph + basis={basis.action.glyph + ? { + dim: `md-`, + ...basis.action.glyph, + } + : { + key: `plus`, + classes: `text-ly${layer}-gl`, + dim: `md-`, + }} + /> + </button> + {/if} + </div> + {/if} + {/if} + </div> + </div> +</div> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-line.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-line.svelte @@ -0,0 +1,57 @@ +<script lang="ts"> + import { fmt_trellis } from "$lib/utils/app"; + import type { ThemeLayer } from "@radroots/themes"; + import type { CallbackPromiseGeneric } from "@radroots/utils"; + import type { Snippet } from "svelte"; + import LoadSymbol from "../lib/load-symbol.svelte"; + + let { + loading = false, + layer, + callback, + hide_border_b, + hide_border_t, + children, + el_end, + }: { + loading?: boolean; + layer: ThemeLayer; + callback?: CallbackPromiseGeneric<MouseEvent>; + hide_border_b: boolean; + hide_border_t: boolean; + children: Snippet; + el_end?: Snippet; + } = $props(); +</script> + +<button + class={`flex flex-row flex-grow overflow-hidden`} + onclick={async (ev) => { + if (callback) await callback(ev); + }} +> + <div + class={`${fmt_trellis(hide_border_b, hide_border_t)} flex flex-row h-full w-full justify-center items-center border-t-line border-ly${layer}-edge el-re`} + > + {#if loading} + <div + class={`flex flex-row h-full w-full justify-center items-center`} + > + <LoadSymbol basis={{ dim: `sm` }} /> + </div> + {:else} + <div + class={`relative group flex flex-row h-line w-full pr-[2px] justify-between items-center el-re`} + > + <div + class={`flex flex-row h-full w-trellis_display justify-between items-center`} + > + {@render children()} + </div> + {#if el_end} + {@render el_end()} + {/if} + </div> + {/if} + </div> +</button> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-offset.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-offset.svelte @@ -0,0 +1,72 @@ +<script lang="ts"> + import type { + ITrellisBasisOffset, + ITrellisBasisOffsetMod, + } from "$lib/types/components/trellis"; + import { Flex, fmt_cl, Glyph } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + import GlyphCircle from "../lib/glyph-circle.svelte"; + import LoadSymbol from "../lib/load-symbol.svelte"; + + let { + basis = undefined, + layer, + }: { + basis?: ITrellisBasisOffset; + layer: ThemeLayer; + } = $props(); + + const mod: ITrellisBasisOffsetMod = $derived(basis?.mod ? basis.mod : `sm`); +</script> + +<div class={`flex flex-row h-full`}> + {#if mod === `sm`} + <div class={`${fmt_cl(``)} flex flex-row h-full w-[22px]`}> + <Flex /> + </div> + {:else if mod === `glyph`} + <div class={`flex flex-row pr-[2px]`}> + <div class={`${fmt_cl(``)} flex flex-row h-full w-trellisOffset`}> + <Flex /> + </div> + </div> + {:else if typeof mod === `object`} + <div + class={`flex flex-row h-full min-w-[20px] w-trellisOffset justify-center items-center pr-3`} + > + <button + class={`fade-in pl-2 translate-x-[3px] translate-y-[1px]`} + onclick={async (ev) => { + if (mod.loading) return; + else if (typeof basis !== `boolean` && basis?.callback) + await basis.callback(ev); + }} + > + {#if mod.loading} + <LoadSymbol basis={{ blades: 8, dim: `xs` }} /> + {:else if `glyph` in mod} + <Glyph + basis={{ + classes: mod.glyph.classes + ? mod.glyph.classes + : `text-ly${layer}-gl`, + ...mod.glyph, + }} + /> + {:else if `glyph_circle` in mod} + <GlyphCircle + basis={{ + classes_wrap: mod.glyph_circle?.classes_wrap, + glyph: { + classes: mod.glyph_circle?.glyph?.classes + ? mod.glyph_circle?.glyph?.classes + : `text-ly${layer}-gl`, + ...mod.glyph_circle?.glyph, + }, + }} + /> + {/if} + </button> + </div> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-row-display-value.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-row-display-value.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + import type { ITrellisKindDisplayValue } from "$lib/types/components/trellis"; + import { get_label_classes_kind } from "$lib/utils/app"; + import { Glyph, fmt_cl } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + + let { + basis, + layer, + hide_active, + }: { + basis: ITrellisKindDisplayValue; + layer: ThemeLayer; + hide_active: boolean; + } = $props(); +</script> + +<button + class={`z-10 flex flex-grow justify-end`} + onclick={async (ev) => { + ev.stopPropagation(); + if (basis.callback) await basis.callback(ev); + }} +> + {#if `icon` in basis} + <Glyph + basis={{ + classes: + basis.icon.classes || + `${get_label_classes_kind(layer, `shade`, hide_active)}`, + key: basis.icon.key, + dim: `sm`, + }} + /> + {:else if basis.label} + {#if `value` in basis.label} + <p + class={`${fmt_cl(basis.label.classes)} font-sans text-line_d_e line-clamp-1 text-ly0-gl-label el-re`} + > + {basis.label.value} + </p> + {/if} + {/if} +</button> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-row-label.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-row-label.svelte @@ -0,0 +1,63 @@ +<script lang="ts"> + import { get_label_classes_kind } from "$lib/utils/app"; + import { type ILabelTupFields, fmt_cl } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + import GlyphButton from "../buttons/glyph-button.svelte"; + + let { + basis, + layer, + hide_active, + }: { + basis: ILabelTupFields; + layer: ThemeLayer; + hide_active: boolean; + } = $props(); +</script> + +<div class={`flex flex-row h-full items-center justify-between`}> + {#if basis.left && basis.left.length} + <div class={`flex flex-row h-full items-center truncate`}> + {#each basis.left as title_l} + <div + class={`${fmt_cl(title_l.classes_wrap)} flex flex-row h-full items-center ${get_label_classes_kind(layer, undefined, hide_active)} ${title_l.hide_truncate ? `` : `truncate`}`} + > + {#if `glyph` in title_l} + <div + class={`flex flex-row justify-start items-center pr-2`} + > + <GlyphButton basis={{ ...title_l.glyph }} /> + </div> + {:else if `value` in title_l} + <p + class={`${fmt_cl(title_l.classes)} font-sans text-line_d ${title_l.hide_truncate ? `` : `truncate`} el-re`} + > + {title_l.value || ``} + </p> + {/if} + </div> + {/each} + </div> + {/if} + {#if basis.right && basis.right.length} + <div + class={`flex flex-row h-full w-content items-center justify-end pr-4`} + > + {#each basis.right.reverse() as title_r} + <div + class={`${fmt_cl(title_r.classes_wrap)} flex flex-row h-full gap-1 items-center ${title_r.hide_truncate ? `` : `truncate`}`} + > + {#if `glyph` in title_r} + <GlyphButton basis={{ ...title_r.glyph }} /> + {:else if `value` in title_r} + <p + class={`${fmt_cl(title_r.classes)} font-sans text-line_d text-ly${layer}-gl_d ${title_r.hide_truncate ? `` : `truncate`} el-re`} + > + {title_r.value || ``} + </p> + {/if} + </div> + {/each} + </div> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-select.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-select.svelte @@ -0,0 +1,65 @@ +<script lang="ts"> + import type { ITrellisBasisSelect } from "$lib/types/components/trellis"; + import type { ThemeLayer } from "@radroots/themes"; + import LoadSymbol from "../lib/load-symbol.svelte"; + import SelectMenu from "../lib/select-menu.svelte"; + import TrellisEnd from "./trellis-end.svelte"; + import TrellisLine from "./trellis-line.svelte"; + import TrellisRowDisplayValue from "./trellis-row-display-value.svelte"; + import TrellisRowLabel from "./trellis-row-label.svelte"; + + let { + basis, + layer, + hide_active, + hide_border_b, + hide_border_t, + }: { + basis: ITrellisBasisSelect; + layer: ThemeLayer; + hide_active: boolean; + hide_border_b: boolean; + hide_border_t: boolean; + } = $props(); + + const loading = $derived( + typeof basis?.loading === `boolean` ? basis.loading : false, + ); + + const value = $derived(basis.el.value); +</script> + +<TrellisLine + {layer} + {loading} + {hide_border_b} + {hide_border_t} + callback={basis.callback} +> + <TrellisRowLabel basis={basis.label} {layer} {hide_active} /> + {#if basis.display} + <div class={`flex flex-row pr-3 justify-center items-end`}> + <SelectMenu {value} basis={basis.el}> + {#if basis.display.loading} + <div + class={`flex flex-row h-full w-full justify-end items-center`} + > + <LoadSymbol basis={{ dim: `sm` }} /> + </div> + {:else} + <TrellisRowDisplayValue + basis={{ ...basis.display }} + {layer} + {hide_active} + /> + {/if} + </SelectMenu> + </div> + {/if} + + {#snippet el_end()} + {#if basis.end} + <TrellisEnd basis={basis.end} {layer} {hide_active} /> + {/if} + {/snippet} +</TrellisLine> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-title.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-title.svelte @@ -0,0 +1,69 @@ +<script lang="ts"> + import type { ITrellisTitle } from "$lib/types/components/trellis"; + import { Flex, fmt_cl, Glyph } from "@radroots/apps-lib"; + import type { ThemeLayer } from "@radroots/themes"; + import LabelSwap from "../lib/label-swap.svelte"; + + let { + basis, + layer = 0, + }: { + basis: ITrellisTitle; + layer: ThemeLayer; + } = $props(); + + const mod = $derived(basis?.mod ? basis.mod : `sm`); +</script> + +<div + class={`${fmt_cl(basis.classes)} flex flex-row h-[24px] w-full pl-[2px] gap-1 items-center`} +> + <button + class={`flex flex-row h-full w-max items-center gap-1 ${mod === `glyph` ? `pl-[36px]` : mod === `sm` ? `pl-[16px]` : ``}`} + onclick={async () => { + if (basis && basis.callback) await basis.callback(); + }} + > + {#if basis.value === true} + <Flex /> + {:else} + <p + class={`font-sans text-trellis_ti text-ly${layer}-gl-label uppercase`} + > + {basis.value || ``} + </p> + {/if} + </button> + {#if basis.link} + <button + class={`${fmt_cl(basis.link.classes)} group flex flex-row h-full w-max items-center`} + onclick={async () => { + if (basis.link && basis.link.callback) + await basis.link.callback(); + }} + > + {#if basis.link.label} + {#if `swap` in basis.link.label} + <LabelSwap basis={basis.link.label} /> + {:else if `value` in basis.link.label} + <p + class={`${fmt_cl(basis.link.label.classes)} font-sans text-trellis_ti uppercase fade-in`} + > + {basis.link.label.value || ``} + </p> + {/if} + {/if} + {#if basis.link.glyph} + <div class={`flex flex-row w-max`}> + <Glyph + basis={{ + ...basis.link.glyph, + dim: `xs-`, + classes: `${fmt_cl(basis.link.glyph.classes)} fade-in`, + }} + /> + </div> + {/if} + </button> + {/if} +</div> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis-touch.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis-touch.svelte @@ -0,0 +1,40 @@ +<script lang="ts"> + import type { ITrellisBasisTouch } from "$lib/types/components/trellis"; + import type { ThemeLayer } from "@radroots/themes"; + import TrellisEnd from "./trellis-end.svelte"; + import TrellisLine from "./trellis-line.svelte"; + import TrellisRowDisplayValue from "./trellis-row-display-value.svelte"; + import TrellisRowLabel from "./trellis-row-label.svelte"; + + let { + basis, + layer, + hide_active, + hide_border_b, + hide_border_t, + }: { + basis: ITrellisBasisTouch; + layer: ThemeLayer; + hide_active: boolean; + hide_border_b: boolean; + hide_border_t: boolean; + } = $props(); +</script> + +<TrellisLine {layer} {hide_border_b} {hide_border_t} callback={basis.callback}> + <TrellisRowLabel basis={basis.label} {layer} {hide_active} /> + {#if basis.display} + <TrellisRowDisplayValue + basis={{ + ...basis.display, + }} + {layer} + {hide_active} + /> + {/if} + {#snippet el_end()} + {#if basis.end} + <TrellisEnd basis={basis.end} {layer} {hide_active} /> + {/if} + {/snippet} +</TrellisLine> diff --git a/apps-lib-pwa/src/lib/components/trellis/trellis.svelte b/apps-lib-pwa/src/lib/components/trellis/trellis.svelte @@ -0,0 +1,149 @@ +<script lang="ts"> + import { app_lo } from "$lib/stores/app"; + import type { ITrellis } from "$lib/types/components/trellis"; + import { fmt_cl, get_context, parse_layer } from "@radroots/apps-lib"; + import type { Snippet } from "svelte"; + import TrellisDefaultLabel from "./trellis-default-label.svelte"; + import TrellisInput from "./trellis-input.svelte"; + import TrellisOffset from "./trellis-offset.svelte"; + import TrellisSelect from "./trellis-select.svelte"; + import TrellisTitle from "./trellis-title.svelte"; + import TrellisTouch from "./trellis-touch.svelte"; + + const { ls } = get_context(`lib`); + + let { + basis, + el_default, + el_offset, + el_append, + }: { + basis: ITrellis; + el_default?: Snippet; + el_offset?: Snippet; + el_append?: Snippet; + } = $props(); + + const hide_border_t = $derived( + typeof basis.hide_border_top === `boolean` + ? basis.hide_border_top + : true, + ); + + const hide_border_b = $derived( + typeof basis.hide_border_bottom === `boolean` + ? basis.hide_border_bottom + : true, + ); + + const hide_rounded = $derived( + typeof basis.hide_rounded === `boolean` ? basis.hide_rounded : false, + ); + + const set_title_background = $derived( + typeof basis.set_title_background === `boolean` + ? basis.set_title_background + : false, + ); + + const set_default_background = $derived( + typeof basis.set_default_background === `boolean` + ? basis.set_default_background + : false, + ); +</script> + +<div + id={basis.id || ``} + class={`${fmt_cl(basis.classes)} flex flex-col`} + data-view={basis.view || ``} +> + <div + class={`relative flex flex-col h-auto w-lo_${$app_lo} gap-[3px] ${set_title_background ? `bg-ly${basis.layer}` : ``}`} + > + {#if basis.title && (!basis.default_el || (basis.default_el && basis.default_el.show_title))} + <TrellisTitle + basis={basis.title} + layer={parse_layer(basis.layer - 1)} + /> + {/if} + {#if basis.default_el} + <div + class={`flex flex-col h-auto w-full justify-center items-center`} + > + {#if el_default} + {@render el_default()} + {:else if basis.default_el} + <TrellisDefaultLabel + layer={parse_layer(basis.layer - 1)} + labels={basis.default_el.labels + ? basis.default_el.labels + : [ + { + label: `${$ls(`common.no_items_to_display`)}.`, + }, + ]} + /> + {/if} + </div> + {:else if basis.list} + <div class={`flex flex-col w-full justify-center items-center`}> + {#each basis.list as li} + {#if li} + <div + class={`${li.hide_field ? `hidden` : ``} group flex flex-row h-full w-full justify-end items-center bg-ly${basis.layer} ${li.full_rounded ? `rounded-touch` : ``} ${hide_rounded ? `` : `first:rounded-t-2xl last:rounded-b-2xl`} ${!li.hide_active ? `active:bg-ly${basis.layer}_a` : ``} el-re`} + > + <div + class={`flex flex-row h-full w-full gap-1 items-center overflow-y-hidden`} + > + {#if !basis.hide_offset} + <TrellisOffset + basis={li.offset} + layer={basis.layer} + /> + {/if} + {#if el_offset} + {@render el_offset()} + {/if} + {#if `touch` in li && li.touch} + <TrellisTouch + basis={li.touch} + layer={basis.layer} + {hide_border_b} + {hide_border_t} + hide_active={!!li.hide_active} + /> + {:else if `input` in li && li.input} + <TrellisInput + basis={li.input} + layer={basis.layer} + {hide_border_b} + {hide_border_t} + /> + {:else if `select` in li && li.select} + <TrellisSelect + basis={li.select} + layer={basis.layer} + {hide_border_b} + {hide_border_t} + hide_active={!!li.hide_active} + /> + {/if} + </div> + </div> + {/if} + {/each} + </div> + {/if} + </div> + {#if el_append} + <div + class={`flex flex-col w-full ${set_default_background ? `bg-ly${basis.layer}` : ``}`} + > + {@render el_append()} + </div> + {/if} +</div> +<div + class={`hidden group-first:border-t-0 group-first:border-t-line group-first:border-b-0 group-first:border-b-line`} +></div> diff --git a/apps-lib-pwa/src/lib/features/farm/farms-add-casli-detail.svelte b/apps-lib-pwa/src/lib/features/farm/farms-add-casli-detail.svelte @@ -0,0 +1,98 @@ +<script lang="ts"> + import FormLineLedger from "$lib/components/forms/form-line-ledger.svelte"; + import { CarouselItem, get_context } from "@radroots/apps-lib"; + import { area_units, form_fields } from "@radroots/utils"; + + const { ls } = get_context(`lib`); + + let { + val_farmname = $bindable(``), + val_farmaddress = $bindable(``), + val_farmarea = $bindable(``), + val_farmarea_unit = $bindable(``), + val_farmcontact = $bindable(``), + farm_geop_lat, + farm_geop_lng, + }: { + val_farmname: string; + val_farmaddress: string; + val_farmarea: string; + val_farmarea_unit: string; + val_farmcontact: string; + farm_geop_lat: string; + farm_geop_lng: string; + } = $props(); +</script> + +<CarouselItem> + <div + class={`flex flex-col h-[100vh] w-full px-6 pt-2 gap-4 justify-start items-center`} + > + <FormLineLedger + bind:value={val_farmaddress} + basis={{ + id: `farm_location`, + label: `${$ls(`common.farm_location`)}`, + input: { + placeholder: `${$ls(`icu.enter_*`, { value: `${$ls(`common.farm_location`)}`.toLowerCase() })}`, + }, + }} + /> + + <FormLineLedger + basis={{ + id: `farm_coordinates`, + label: `${$ls(`common.farm_coordinates`)}`, + display_value: + farm_geop_lat && farm_geop_lng + ? `${farm_geop_lat}, ${farm_geop_lng}` + : undefined, + input: + farm_geop_lat && farm_geop_lng + ? undefined + : { + placeholder: `${$ls(`icu.enter_*`, { value: `${$ls(`common.farm_coordinates`)}`.toLowerCase() })}`, + }, + }} + /> + <FormLineLedger + bind:value={val_farmname} + basis={{ + id: `farm_name`, + label: `${$ls(`common.farm_name`)}`, + input: { + placeholder: `${$ls(`icu.enter_*`, { value: `${$ls(`common.farm_name`)}`.toLowerCase() })}`, + }, + }} + /> + <FormLineLedger + bind:value={val_farmarea} + bind:value_label_sel={val_farmarea_unit} + basis={{ + id: `farm_size`, + label: `${$ls(`common.farm_size`)}`, + label_select: { + label: `${$ls(`units.area.${val_farmarea_unit}_ab`)}`, + entries: area_units.map((i) => ({ + value: i, + label: `${$ls(`units.area.${i}`)}`, + })), + }, + input: { + placeholder: `${`${$ls(`icu.enter_*`, { value: `${$ls(`common.farm_size`)}`.toLowerCase() })}`} ${`${$ls(`units.area.${val_farmarea_unit}_pl`)}`.toLowerCase()}`, + field: form_fields.farm_size, + }, + }} + /> + <FormLineLedger + bind:value={val_farmcontact} + basis={{ + id: `farm_contact`, + label: `${$ls(`common.farm_contact`)}`, + input: { + placeholder: `${$ls(`icu.enter_*`, { value: `${$ls(`common.contact_name`)}`.toLowerCase() })}`, + }, + }} + /> + </div> +</CarouselItem> diff --git a/apps-lib-pwa/src/lib/features/farm/farms-add-casli-map.svelte b/apps-lib-pwa/src/lib/features/farm/farms-add-casli-map.svelte @@ -0,0 +1,94 @@ +<script lang="ts"> + import WrapBorder from "$lib/components/lib/wrap-border.svelte"; + import MapMarkerArea from "$lib/components/map/map-marker-area.svelte"; + import Map from "$lib/components/map/map.svelte"; + import { app_lo } from "$lib/stores/app"; + import { focus_map_marker } from "$lib/utils/map"; + import { + CarouselItem, + Fade, + geop_is_valid, + get_context, + } from "@radroots/apps-lib"; + import { + handle_err, + type GeocoderReverseResult, + type GeolocationPoint, + } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { lc_geop_current, lc_geocode } = get_context(`lib`); + + let { + map_geoc = $bindable(undefined), + map_geop = $bindable(undefined), + farm_geop_lat, + farm_geop_lng, + }: { + map_geoc: GeocoderReverseResult | undefined; + map_geop: GeolocationPoint | undefined; + farm_geop_lat: string; + farm_geop_lng: string; + } = $props(); + + let map: maplibregl.Map | undefined = $state(undefined); + + const is_valid_geop = $derived(geop_is_valid(map_geop)); + + onMount(async () => { + try { + const geop = await lc_geop_current(); + if (!geop) return; + map_geop = { ...geop }; + const geoc = await lc_geocode(geop); + if (!geoc) return; + map_geoc = geoc; + if (map && map_geop) map.setCenter([map_geop.lng, map_geop.lat]); + focus_map_marker(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); +</script> + +<CarouselItem> + <div + class={`flex flex-col h-[100vh] w-full px-6 gap-4 justify-start items-center`} + > + <WrapBorder basis={{ classes: `h-lo_view_main_${$app_lo}` }}> + <Map bind:map> + {#if map_geop} + <MapMarkerArea + bind:map_geop + bind:map_geoc + basis={{ + show_display: true, + }} + /> + {/if} + </Map> + </WrapBorder> + {#if is_valid_geop} + <Fade> + <div + class={`flex flex-col w-full gap-1 justify-center items-center`} + > + <div + class={`flex flex-row w-full gap-2 justify-center items-center`} + > + <p + class={`font-sans font-[500] text-ly0-gl tracking-tightest`} + > + {farm_geop_lat} + </p> + <p + class={`font-sans font-[500] text-ly0-gl tracking-tightest`} + > + {farm_geop_lng} + </p> + </div> + </div> + </Fade> + {/if} + </div> +</CarouselItem> diff --git a/apps-lib-pwa/src/lib/features/farm/farms-display-li-el.svelte b/apps-lib-pwa/src/lib/features/farm/farms-display-li-el.svelte @@ -0,0 +1,109 @@ +<script lang="ts"> + import MapMarkerArea from "$lib/components/map/map-marker-area.svelte"; + import Map from "$lib/components/map/map.svelte"; + import type { FarmExtended } from "$lib/views/farms/farm"; + import { get_context } from "@radroots/apps-lib"; + import { + fmt_geolocation_address, + geol_lat_fmt, + geol_lng_fmt, + parse_geol_point_tup, + parse_tup_geop_point, + type CallbackPromiseGeneric, + type GeolocationPointTuple, + } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { ls, locale } = get_context(`lib`); + + let { + basis, + on_handle_farm_view, + }: { + basis: FarmExtended; + on_handle_farm_view: CallbackPromiseGeneric<string>; + } = $props(); + + let map: maplibregl.Map | undefined = $state(undefined); + let map_center: GeolocationPointTuple = $state([0, 0]); + + onMount(async () => { + if (basis.location?.point) + map_center = parse_geol_point_tup(basis.location.point); + if (map) map.setCenter(map_center); + }); + + const map_geop = $derived(parse_tup_geop_point(map_center)); + + const farm_addr_fmt = $derived( + basis.location?.address + ? fmt_geolocation_address(basis.location.address) + : ``, + ); + + const farm_geop_lat = $derived( + basis.location?.point + ? geol_lat_fmt(basis.location.point.lat, `dms`, $locale, 3) + : ``, + ); + + const farm_geop_lng = $derived( + basis.location?.point + ? geol_lng_fmt(basis.location.point.lng, `dms`, $locale, 3) + : ``, + ); +</script> + +<button + class={`z-10 relative flex flex-col w-full p-4 gap-3 justify-start items-center bg-ly1 ly1-active-raise-less ly1-active-ring rounded-3xl el-re`} + onclick={async () => { + if (basis.farm.id) await on_handle_farm_view(basis.farm.id); + }} +> + <div class={`flex flex-col w-full gap-2 justify-center items-center`}> + <div class={`flex flex-row w-full justify-between items-center`}> + <p class={`font-sans font-[500] text-3xl text-ly0-gl`}> + {basis.farm.name} + </p> + + <div + class={`flex flex-row h-6 px-2 py-1 justify-center items-center bg-lime-400 rounded-lg`} + > + <p class={`font-sans font-[700] text-white`}> + {`${$ls(`common.farm`)}`} + </p> + </div> + </div> + <div class={`flex flex-col w-full justify-center items-center`}> + <div class={`flex flex-row w-full justify-start items-center`}> + <p class={`font-sans font-[500] text-lg text-ly0-gl`}> + {farm_addr_fmt} + </p> + </div> + <div class={`flex flex-row w-full justify-start items-center`}> + <p class={`font-sans font-[500] text-lg text-ly0-gl`}> + {farm_geop_lat && farm_geop_lng + ? `${farm_geop_lat}, ${farm_geop_lng}` + : ``} + </p> + </div> + </div> + </div> + <div + class={`flex flex-col h-[16rem] w-full justify-center items-center rounded-2xl overflow-hidden`} + > + <Map + bind:map + basis={{ + interactive: false, + }} + > + <MapMarkerArea + {map_geop} + basis={{ + no_drag: true, + }} + /> + </Map> + </div> +</button> diff --git a/apps-lib-pwa/src/lib/features/farm/farms-products-review-card.svelte b/apps-lib-pwa/src/lib/features/farm/farms-products-review-card.svelte @@ -0,0 +1,124 @@ +<script lang="ts"> + import { app_lo } from "$lib/stores/app"; + import type { IViewFarmsProductsAddSubmitPayload } from "$lib/types/views"; + import { get_context, Glyph, ImagePath, symbols } from "@radroots/apps-lib"; + import { + parse_currency_marker, + parse_geocode_address, + } from "@radroots/utils"; + + const { ls, locale } = get_context(`lib`); + + let { + basis, + }: { + basis: { + data: IViewFarmsProductsAddSubmitPayload | undefined; + }; + } = $props(); + + //@todo +</script> + +<div + class={`flex flex-col h-[20rem] w-lo_line_entry_${$app_lo} justify-start items-start rounded-touch bg-ly1 overflow-hidden`} +> + <div class={`flex flex-row h-[10rem] w-full justify-center items-center`}> + {#if basis.data?.photos.length} + <ImagePath + basis={{ + path: basis.data.photos[0], + }} + /> + {:else} + <div + class={`flex flex-row h-full w-full justify-center items-center bg-ly2`} + > + <div class={`flex flex-col justify-start items-center`}> + <Glyph + basis={{ + classes: `text-ly0-gl`, + dim: `sm`, + key: `image-broken`, + }} + /> + <p class={`font-sans font-[400] text-sm text-ly0-gl`}> + {`No photo`} + </p> + </div> + </div> + {/if} + </div> + <div + class={`flex flex-col h-[10rem] w-full px-3 py-2 justify-start items-center`} + > + {#if basis.data} + {@const data_geoc_address = parse_geocode_address( + basis.data.geocode_result, + )} + <div class={`flex flex-row w-full justify-between items-center`}> + <div class={`flex flex-row gap-1 justify-start items-center`}> + <p + class={`font-sans font-[600] text-xl text-th-black capitalize`} + > + {basis.data.product} + </p> + </div> + <div + class={`flex flex-row gap-[2px] justify-start items-center`} + > + <p class={`font-sans font-[600] text-xl text-th-black`}> + {`${parse_currency_marker($locale, basis.data.price_currency)}${basis.data.price_amount}`} + </p> + <p class={`font-sans font-[600] text-xl text-th-black`}> + {`/`} + </p> + <p class={`font-sans font-[600] text-xl text-th-black`}> + {`${$ls(`units.mass.unit.${basis.data.price_quantity_unit}_ab`)}`} + </p> + </div> + </div> + <div class={`flex flex-row w-full justify-between items-center`}> + <div class={`flex flex-row gap-1 justify-start items-center`}> + <p + class={`font-sans font-[600] text-lg text-ly1-gl capitalize`} + > + {basis.data.process} + </p> + <p class={`font-sans font-[600] text-xl text-ly1-gl`}> + {symbols.bullet} + </p> + <p class={`font-sans font-[600] text-lg text-ly1-gl`}> + {`${basis.data.quantity_amount} ${$ls(`units.mass.unit.${basis.data.quantity_unit}_ab`)} ${basis.data.quantity_label}`} + </p> + </div> + </div> + <div class={`flex flex-row w-full justify-start items-center`}> + <p + class={`font-sans font-[400] text-sm text-th-black capitalize line-clamp-2 overflow-hidden text-ellipsis`} + > + {basis.data.description} + </p> + </div> + {#if data_geoc_address} + <div + class={`flex flex-row w-full pt-2 justify-between items-center`} + > + <div + class={`flex flex-row gap-1 justify-start items-center`} + > + <p class={`font-sans font-[600] text-th-black`}> + {`${data_geoc_address.primary}, ${data_geoc_address.admin}`} + </p> + <p class={`font-sans font-[600] text-th-black`}> + {symbols.bullet} + </p> + <p class={`font-sans font-[600] text-th-black`}> + {`${data_geoc_address.country}`} + </p> + </div> + </div> + {/if} + {/if} + </div> +</div> diff --git a/apps-lib-pwa/src/lib/index.ts b/apps-lib-pwa/src/lib/index.ts @@ -1 +1,73 @@ -export const rad = `roots` -\ No newline at end of file +export * from "./client/keystore-nostr.js" +export * from "./stores/app.js" +export * from "./stores/nostr.js" +export * from "./styles/lib.js" +export * from "./types/app.js" +export * from "./types/components.js" +export * from "./types/components/trellis.js" +export * from "./types/views.js" +export * from "./utils/app.js" +export * from "./utils/context.js" +export * from "./utils/map.js" +export * from "./utils/schemas/farm.js" +export * from "./utils/schemas/location-gcs.js" +export * from "./views/farms/farm.js" +export * from "./views/profile/types.js" +export { default as ButtonLabelDashed } from "./components/buttons/button-label-dashed.svelte" +export { default as ButtonLayout } from "./components/buttons/button-layout.svelte" +export { default as ButtonLayoutPair } from "./components/buttons/button-layout-pair.svelte" +export { default as ButtonRoundNav } from "./components/buttons/button-round-nav.svelte" +export { default as ButtonSimple } from "./components/buttons/button-simple.svelte" +export { default as Css } from "./components/lib/css.svelte" +export { default as EntryLine } from "./components/forms/entry-line.svelte" +export { default as EntryWrap } from "./components/forms/entry-wrap.svelte" +export { default as Farms } from "./views/farms/farms.svelte" +export { default as FarmsAdd } from "./views/farms/farms-add.svelte" +export { default as FarmsAddCasliDetail } from "./features/farm/farms-add-casli-detail.svelte" +export { default as FarmsAddCasliMap } from "./features/farm/farms-add-casli-map.svelte" +export { default as FarmsDetails } from "./views/farms/farms-details.svelte" +export { default as FarmsDisplayLiEl } from "./features/farm/farms-display-li-el.svelte" +export { default as FarmsProductsReviewCard } from "./features/farm/farms-products-review-card.svelte" +export { default as FloatPage } from "./components/lib/float-page.svelte" +export { default as FormLineLedger } from "./components/forms/form-line-ledger.svelte" +export { default as FormLineLedgerLabelSelectLabel } from "./components/forms/form-line-ledger-label-select-label.svelte" +export { default as FormLineLedgerSelect } from "./components/forms/form-line-ledger-select.svelte" +export { default as GlyphButton } from "./components/buttons/glyph-button.svelte" +export { default as GlyphButtonSimple } from "./components/buttons/glyph-button-simple.svelte" +export { default as GlyphCircle } from "./components/lib/glyph-circle.svelte" +export { default as Home } from "./views/home.svelte" +export { default as ImageUploadAddPhoto } from "./components/lib/image-upload-add-photo.svelte" +export { default as InputPwa } from "./components/lib/input-pwa.svelte" +export { default as InputValue } from "./components/lib/input-value.svelte" +export { default as LabelSwap } from "./components/lib/label-swap.svelte" +export { default as LayoutBottomButton } from "./components/layouts/layout-bottom-button.svelte" +export { default as LayoutPage } from "./components/layouts/layout-page.svelte" +export { default as LayoutTrellis } from "./components/layouts/layout-trellis.svelte" +export { default as LayoutView } from "./components/layouts/layout-view.svelte" +export { default as LayoutWindow } from "./components/layouts/layout-window.svelte" +export { default as LoadSymbol } from "./components/lib/load-symbol.svelte" +export { default as LogoCircle } from "./components/lib/logo-circle.svelte" +export { default as LogoCircleSm } from "./components/lib/logo-circle-sm.svelte" +export { default as LogoLetters } from "./components/lib/logo-letters.svelte" +export { default as Map } from "./components/map/map.svelte" +export { default as MapMarkerArea } from "./components/map/map-marker-area.svelte" +export { default as MapMarkerAreaDisplay } from "./components/map/map-marker-area-display.svelte" +export { default as NavigationTabs } from "./components/navigation/navigation-tabs.svelte" +export { default as PageHeader } from "./components/navigation/page-header.svelte" +export { default as PageToolbar } from "./components/navigation/page-toolbar.svelte" +export { default as Profile } from "./views/profile/profile.svelte" +export { default as SelectMenu } from "./components/lib/select-menu.svelte" +export { default as SelectPwa } from "./components/lib/select-pwa.svelte" +export { default as Settings } from "./views/settings.svelte" +export { default as Trellis } from "./components/trellis/trellis.svelte" +export { default as TrellisDefaultLabel } from "./components/trellis/trellis-default-label.svelte" +export { default as TrellisEnd } from "./components/trellis/trellis-end.svelte" +export { default as TrellisInput } from "./components/trellis/trellis-input.svelte" +export { default as TrellisLine } from "./components/trellis/trellis-line.svelte" +export { default as TrellisOffset } from "./components/trellis/trellis-offset.svelte" +export { default as TrellisRowDisplayValue } from "./components/trellis/trellis-row-display-value.svelte" +export { default as TrellisRowLabel } from "./components/trellis/trellis-row-label.svelte" +export { default as TrellisSelect } from "./components/trellis/trellis-select.svelte" +export { default as TrellisTitle } from "./components/trellis/trellis-title.svelte" +export { default as TrellisTouch } from "./components/trellis/trellis-touch.svelte" +export { default as WrapBorder } from "./components/lib/wrap-border.svelte" diff --git a/apps-lib-pwa/src/lib/stores/app.ts b/apps-lib-pwa/src/lib/stores/app.ts @@ -0,0 +1,33 @@ +import type { BrowserPlatformInfo, NavigationPreviousParam, NavigationRouteParamField, NavigationRouteParamId, NavigationRouteParamLat, NavigationRouteParamLng, NavigationRouteParamNostrPublicKey, NavigationRouteParamRef } from "@radroots/apps-lib"; +import { writable } from "svelte/store"; +import { queryParam } from "sveltekit-search-params"; + +export const app_tilt = writable<boolean>(false); +export const app_lo = writable<string>(""); +export const app_notify = writable<string>(``); +export const app_splash = writable<boolean>(true); +export const app_loading = writable<boolean>(false); +export const app_platform = writable<BrowserPlatformInfo | undefined>(undefined); + +export const cfg_role = writable<string>(); +export const cfg_setup = writable<boolean | undefined>(undefined); + +export const envelope_visible = writable<boolean>(false); +export const envelope_tilt = writable<boolean>(true); + +export const nav_visible = writable<boolean>(false); +export const nav_blur = writable<boolean>(false); +export const nav_prev = writable<NavigationPreviousParam<string>[]>([]); + +export const ph_blur = writable<boolean>(false); + +export const tabs_visible = writable<boolean>(false); +export const tabs_blur = writable<boolean>(false); +export const tabs_active = writable<number>(0); + +export const qp_id = queryParam<NavigationRouteParamId>("id"); +export const qp_field = queryParam<NavigationRouteParamField>("field"); +export const qp_ref = queryParam<NavigationRouteParamRef>("ref"); +export const qp_lat = queryParam<NavigationRouteParamLat>("lat"); +export const qp_lng = queryParam<NavigationRouteParamLng>("lng"); +export const qp_keynostr = queryParam<NavigationRouteParamNostrPublicKey>("key_nostr"); diff --git a/apps-lib-pwa/src/lib/stores/nostr.ts b/apps-lib-pwa/src/lib/stores/nostr.ts @@ -0,0 +1,16 @@ +import { writable } from "svelte/store"; + +export const nostr_ndk_configured = writable(false); + +export const nostr_sync_prevent = writable(false); +export const nostr_sync_attempts = writable(0); +export const nostr_sync_attempts_max = writable(8); +export const nostr_sync_stop = writable(true); + +export const nostr_poll_relays_prevent = writable(false); +export const nostr_poll_relays_attempts = writable(0); +export const nostr_poll_relays_attempts_max = writable(0); +export const nostr_poll_relays_stop = writable(true); + +export const nostr_relays_connected = writable<string[]>([]); + diff --git a/apps-lib-pwa/src/lib/styles/lib.ts b/apps-lib-pwa/src/lib/styles/lib.ts @@ -0,0 +1,10 @@ +import type { LoadingDimension } from "@radroots/apps-lib"; + +export const loading_style_map: Map<LoadingDimension, { dim_1: number; gl_2: number }> = new Map([ + ["glyph-send-button", { dim_1: 20, gl_2: 20 }], + ["xs", { dim_1: 12, gl_2: 12 }], + ["sm", { dim_1: 16, gl_2: 16 }], + ["md", { dim_1: 20, gl_2: 20 }], + ["lg", { dim_1: 28, gl_2: 28 }], + ["xl", { dim_1: 36, gl_2: 36 }], +]); diff --git a/apps-lib-pwa/src/lib/types/app.ts b/apps-lib-pwa/src/lib/types/app.ts @@ -0,0 +1,28 @@ +export type AppConfigRole = `farmer` | `personal` + +export type AppLayoutKeyIOS = `ios0` | `ios1`; +export type AppLayoutKeyWeb = `webm0` | `webm1`; +export type AppLayoutKey = AppLayoutKeyIOS | AppLayoutKeyWeb; + +export type AppLayoutIOS<T extends string> = `${T}_${AppLayoutKeyIOS}`; +export type AppLayoutWeb<T extends string> = `${T}_${AppLayoutKeyWeb}`; + +export type AppLayoutKeyHeight = + | `lo_view_main` + | `lo_bottom_button` + | `nav_tabs` + | `nav_page_header` + | `nav_page_toolbar`; + +export type AppLayoutKeyWidth = + | `lo` + | `lo_line_entry` + | `lo_textdesc`; + +export type AppHeightsResponsiveIOS = AppLayoutIOS<AppLayoutKeyHeight>; +export type AppHeightsResponsiveWeb = AppLayoutWeb<AppLayoutKeyHeight>; + +export type AppWidthsResponsiveIOS = AppLayoutIOS<AppLayoutKeyWidth>; +export type AppWidthsResponsiveWeb = AppLayoutWeb<AppLayoutKeyWidth>; + +export type LabelFieldKind = `link` | `on` | `shade`; diff --git a/apps-lib-pwa/src/lib/types/components.ts b/apps-lib-pwa/src/lib/types/components.ts @@ -0,0 +1,33 @@ +import type { CallbackRoute, GeometryScreenPositionHorizontal, ICb, ICbOpt, IDisabledOpt, IGlyph, IGlyphKey, ILoadingOpt, ILyOpt } from "@radroots/apps-lib"; +import type { CallbackPromise } from "@radroots/utils"; + +export type IButtonSimple = ILyOpt & { + label: string; + callback: CallbackPromise; + allow_propogation?: boolean; +}; + +export type IPageHeader<T extends string> = { + label: string; + callback_route?: CallbackRoute<T>; +}; + +export type IPageToolbar<T extends string> = ICbOpt & { + header?: IPageHeader<T>; +}; + +export type IMapMarkerArea = { + show_display?: boolean; + no_drag?: boolean; +} + +export type IGlyphCircle = { + classes_wrap: string; + glyph: IGlyph +}; + +export type IFloatPage = { + posx: Omit<GeometryScreenPositionHorizontal, "center">; +}; + +export type IButtonNavRound = ICb & IDisabledOpt & ILoadingOpt & IGlyphKey; diff --git a/apps-lib-pwa/src/lib/types/components/trellis.ts b/apps-lib-pwa/src/lib/types/components/trellis.ts @@ -0,0 +1,123 @@ +import type { GlyphKey, ICbGOpt, ICbOpt, IClOpt, IGl, IGlOpt, IGlyph, IInput, ILabel, ILabelOpt, ILabelTup, ILoadingOpt, ILy, ISelect } from "@radroots/apps-lib"; +import type { CallbackPromise } from "@radroots/utils"; +import type { IGlyphCircle } from "../components"; + +export type ITrellis = ILy & + IClOpt & + ITrellisStyles & { + id?: string; + view?: string; + title?: ITrellisTitle; + description?: ITrellisDescription; + default_el?: ITrellisDefault; + list?: (ITrellisKind | undefined)[]; + hide_offset?: true; + }; + +export type ITrellisStyles = { + hide_rounded?: boolean; + hide_border_top?: boolean; + hide_border_bottom?: boolean; + set_title_background?: boolean; + set_default_background?: boolean; +}; + +export type ITrellisTitle = ICbOpt & + IClOpt & { + mod?: ITrellisBasisOffsetMod, + value: string | true; + link?: ICbOpt & + IClOpt & + IGlOpt & ILabelOpt; + }; + +export type ITrellisDescription = string | true; + +export type ITrellisBasisOffsetModKey = 'sm' | 'glyph'; +export type ITrellisBasisOffsetMod = ITrellisBasisOffsetModKey | (({ glyph: IGlyph } | { glyph_circle: IGlyphCircle }) & { + loading?: boolean; +}); + +export type ITrellisDefault = { + labels?: ITrellisDefaultLabel[]; + show_title?: boolean; +}; + +export type ITrellisDefaultLabel = ICbOpt & { + label: string; + classes?: string; +}; + +export type ITrellisKind = ( + | ITrellisKindTouch + | ITrellisKindInput + | ITrellisKindSelect +); + +export type ITrellisBasis = { + loading?: boolean; + hide_active?: boolean; + hide_field?: boolean; + offset?: ITrellisBasisOffset; + full_rounded?: boolean; +}; + +export type ITrellisBasisOffset = ICbGOpt<MouseEvent> & + IClOpt & { + mod?: ITrellisBasisOffsetMod; + classes?: string; + hide_space?: boolean; + hide_offset?: boolean; + }; + +export type ITrellisKindDisplay = { + display?: ITrellisKindDisplayValue; +} +export type ITrellisKindDisplayValue = ICbGOpt<MouseEvent> & ILoadingOpt & + (ITrellisKindDisplayValueIcon | ILabel); + + +export type ITrellisKindDisplayValueIcon = { + icon: { + classes?: string; + key: GlyphKey; + }; +}; +export type ITrellisKindTouch = ITrellisBasis & { + touch: ITrellisBasisTouch; +}; + +export type ITrellisBasisTouch = ICbGOpt<MouseEvent> & + ILabelTup & ITrellisKindDisplay & { + end?: ITrellisBasisTouchEnd; + }; + +export type ITrellisKindInput = ITrellisBasis & { + input: ITrellisBasisInput; +}; + +export type ITrellisBasisInput = { + basis: IInput<string>; + line_label?: { + classes?: string; + value: string; + }; + action?: { + visible: boolean; + loading?: boolean; + callback?: CallbackPromise; + glyph?: IGlyph + }; +}; + +export type ITrellisKindSelect = ITrellisBasis & { + select: ITrellisBasisSelect; +}; + +export type ITrellisBasisSelect = ICbGOpt<MouseEvent> & + ILabelTup & ITrellisKindDisplay & ILoadingOpt & { + end?: ITrellisBasisTouchEnd; + el: ISelect & { value: string; }; + }; + +export type ITrellisBasisTouchEnd = ICbGOpt<MouseEvent> & IGl; +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/types/views.ts b/apps-lib-pwa/src/lib/types/views.ts @@ -0,0 +1,43 @@ +import type { CallbackPromise, GeocoderReverseResult, GeolocationPoint } from "@radroots/utils"; +import type { FarmExtended } from "../views/farms/farm"; + +export type IViewBasis<T extends object> = { + kv_init_prevent?: boolean; + on_mount?: CallbackPromise; + on_destroy?: CallbackPromise; +} & T; + +export type IViewHomeData = {}; + +export type IViewFarmsData = { + list: FarmExtended[]; +}; + +export type IViewFarmsDetailsData = FarmExtended; + +export type IViewFarmsProductsAddData = FarmExtended; + +export type IViewFarmsProductsAddSubmitPayload = { + product: string; + process: string; + description: string; + price_amount: number; + price_currency: string; + price_quantity_unit: string; + photos: string[]; + quantity_amount: number; + quantity_unit: string; + quantity_label: string; + geolocation_point: GeolocationPoint; + geocode_result: GeocoderReverseResult; +}; + +export type IViewFarmsAddSubmission = { + farm_name: string; + farm_area?: number; + farm_area_unit?: string; + farm_contact_name?: string; + geolocation_point: GeolocationPoint; + geocode_result: GeocoderReverseResult; +}; + diff --git a/apps-lib-pwa/src/lib/utils/app.ts b/apps-lib-pwa/src/lib/utils/app.ts @@ -0,0 +1,39 @@ +import type { AppLayoutKey, LabelFieldKind } from "$lib/types/app"; +import type { ThemeLayer } from "@radroots/themes"; + +type ConfigWindow = { + layout: Record<AppLayoutKey, { + h: number; + }>; + debounce: { + search: number; + } +}; + +export const cfg_app: ConfigWindow = { + layout: { + ios0: { + h: 600 + }, + ios1: { + h: 750 + }, + webm0: { + h: 600 + }, + webm1: { + h: 750 + } + }, + debounce: { + search: 200 + }, +}; + +export const fmt_trellis = (hide_border_t: boolean, hide_border_b: boolean): string => { + return `${hide_border_t ? `group-first:border-t-0` : `group-first:border-t-line`} ${hide_border_b ? `group-last:border-b-0` : `group-last:border-b-line`}`; +}; + +export const get_label_classes_kind = (layer: ThemeLayer, label_kind: LabelFieldKind | undefined, hide_active: boolean): string => { + return `text-ly${layer}-gl${label_kind ? `-${label_kind}` : ``} ${hide_active ? `` : `group-active:text-ly${layer}-gl${label_kind ? `-${label_kind}_a` : `_a`}`}` +}; +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/utils/context.ts b/apps-lib-pwa/src/lib/utils/context.ts @@ -0,0 +1,21 @@ +import type { I18nTranslateFunction, I18nTranslateLocale, LocalCallbackColorMode, LocalCallbackGeocode, LocalCallbackGeocodeCurrent, LocalCallbackGuiAlert, LocalCallbackGuiConfirm, LocalCallbackImgBin, LocalCallbackPhotosAddMultiple, LocalCallbackPhotosUpload } from "@radroots/apps-lib"; + +export type ContextKeys = + | `lib`; + +export type ContextMap = { + lib: LibContext; +}; + +export type LibContext = { + ls: I18nTranslateFunction; + locale: I18nTranslateLocale; + lc_color_mode: LocalCallbackColorMode; + lc_gui_alert: LocalCallbackGuiAlert; + lc_gui_confirm: LocalCallbackGuiConfirm; + lc_geocode: LocalCallbackGeocode; + lc_photos_add: LocalCallbackPhotosAddMultiple; + lc_img_bin: LocalCallbackImgBin; + lc_geop_current: LocalCallbackGeocodeCurrent; + lc_photos_upload: LocalCallbackPhotosUpload; +}; +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/utils/map.ts b/apps-lib-pwa/src/lib/utils/map.ts @@ -0,0 +1,24 @@ +export const cfg_map = { + styles: { + base: { + light: `https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json`, + dark: `https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json` + } + }, + popup: { + dot: { + offset: [0, -10] as [number, number] + } + }, + coords: { + default: { + lat: 0, + lng: 0, + } + } +}; + +export const focus_map_marker = (): void => { + const el = document.querySelector(".maplibregl-marker"); + if (el instanceof HTMLElement) el.click(); +}; +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/utils/schemas/farm.ts b/apps-lib-pwa/src/lib/utils/schemas/farm.ts @@ -0,0 +1,28 @@ +import { dev } from "$app/environment"; +import type { IViewFarmsAddSubmission, IViewFarmsProductsAddSubmitPayload } from "$lib/types/views"; +import { form_fields, schema_geocode_result, schema_geolocation_point, util_rxp, zf_numf_pos, zf_numi_pos, zf_price } from "@radroots/utils"; +import { z } from "zod"; + +export const schema_view_farms_add_submission: z.ZodSchema<IViewFarmsAddSubmission> = z.object({ + farm_name: z.string().regex(form_fields.farm_name.validate), + farm_area: zf_numf_pos.optional(), + farm_area_unit: z.string().regex(form_fields.area_unit.validate).optional(), + farm_contact_name: z.string().regex(form_fields.contact_name.validate).optional(), + geolocation_point: schema_geolocation_point, + geocode_result: schema_geocode_result, +}); + +export const schema_view_farms_products_add_submission: z.ZodSchema<IViewFarmsProductsAddSubmitPayload> = z.object({ + product: z.string().regex(form_fields.product_key.validate), + process: z.string().regex(form_fields.product_process.validate), + description: z.string().regex(form_fields.product_description.validate), + price_amount: zf_price, + price_currency: z.string().regex(form_fields.price_currency.validate), + price_quantity_unit: z.string().regex(form_fields.quantity_unit.validate), + photos: z.array(z.string().regex(dev ? util_rxp.url_image_upload_dev : util_rxp.url_image_upload)), + quantity_amount: zf_numi_pos, + quantity_unit: z.string().regex(form_fields.quantity_unit.validate), + quantity_label: z.string().regex(form_fields.quantity_label.validate), + geolocation_point: schema_geolocation_point, + geocode_result: schema_geocode_result, +}); +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/utils/schemas/location-gcs.ts b/apps-lib-pwa/src/lib/utils/schemas/location-gcs.ts @@ -0,0 +1,22 @@ +import type { LocationGcs } from "@radroots/tangle-schema-bindings"; +import type { LocationBasis } from "@radroots/utils"; + +export const location_gcs_to_location_basis = ({ + id, + lat, + lng, + gc_name: primary, + gc_admin1_name: admin, + gc_country_id: country, +}: LocationGcs): LocationBasis => ({ + id, + point: { + lat, + lng, + }, + address: primary && admin && country ? { + primary, + admin, + country, + } : undefined +}); +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/views/farms/farm.ts b/apps-lib-pwa/src/lib/views/farms/farm.ts @@ -0,0 +1,14 @@ +import type { Farm } from "@radroots/tangle-schema-bindings"; +import type { LocationBasis } from "@radroots/utils"; + +export type FarmExtended = { + farm: Farm; + location?: LocationBasis; + lots?: FarmLotBasis[]; +}; + + +export type FarmLotBasis = { + id: string; + location?: LocationBasis; +}; +\ No newline at end of file diff --git a/apps-lib-pwa/src/lib/views/farms/farms-add.svelte b/apps-lib-pwa/src/lib/views/farms/farms-add.svelte @@ -0,0 +1,223 @@ +<script lang="ts"> + import ButtonLayoutPair from "$lib/components/buttons/button-layout-pair.svelte"; + import LayoutBottomButton from "$lib/components/layouts/layout-bottom-button.svelte"; + import LayoutView from "$lib/components/layouts/layout-view.svelte"; + import PageToolbar from "$lib/components/navigation/page-toolbar.svelte"; + import FarmsAddCasliDetail from "$lib/features/farm/farms-add-casli-detail.svelte"; + import FarmsAddCasliMap from "$lib/features/farm/farms-add-casli-map.svelte"; + import { app_platform } from "$lib/stores/app"; + import type { IViewFarmsAddSubmission } from "$lib/types/views"; + import { focus_map_marker } from "$lib/utils/map"; + import { schema_view_farms_add_submission } from "$lib/utils/schemas/farm"; + import { + Carousel, + casl_dec, + casl_i, + casl_inc, + casl_init, + el_id, + fmt_id, + geop_init, + geop_is_valid, + get_context, + type CallbackRoute, + } from "@radroots/apps-lib"; + import { + geol_lat_fmt, + geol_lng_fmt, + handle_err, + parse_float, + parse_geocode_address, + type CallbackPromiseGeneric, + type GeocoderReverseResult, + type GeolocationAddress, + type GeolocationPoint, + } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { ls, locale, lc_gui_alert, lc_geop_current, lc_geocode } = + get_context(`lib`); + + let { + basis, + }: { + basis: { + callback_route?: CallbackRoute<string>; + on_submit: CallbackPromiseGeneric<{ + payload: IViewFarmsAddSubmission; + }>; + }; + } = $props(); + + let map_geop: GeolocationPoint = $state(geop_init()); + let map_geoc: GeocoderReverseResult | undefined = $state(undefined); + + let val_farmname = $state(``); + let val_farmaddress = $state(``); + let val_farmcontact = $state(``); + let val_farmarea = $state(``); + let val_farmarea_unit = $state(`ac`); + + const disabled_submit = $derived($casl_i === 1 && !val_farmname); + + onMount(async () => { + try { + casl_init(0, 2); + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + const farm_geop_lat = $derived( + geop_is_valid(map_geop) + ? geol_lat_fmt(map_geop.lat, `dms`, $locale, 3) + : ``, + ); + + const farm_geop_lng = $derived( + geop_is_valid(map_geop) + ? geol_lng_fmt(map_geop.lng, `dms`, $locale, 3) + : ``, + ); + + const farm_geolocation_address: GeolocationAddress | undefined = $derived( + parse_geocode_address(map_geoc), + ); + + $effect(() => { + if (farm_geolocation_address) + val_farmaddress = `${farm_geolocation_address.primary}, ${farm_geolocation_address.admin}, ${farm_geolocation_address.country}`; + }); + + const handle_enter_location = async (): Promise<void> => { + map_geoc = undefined; + map_geop = geop_init(); + val_farmaddress = ``; + await handle_continue(); + el_id(fmt_id(`farm_location`))?.focus(); + }; + + const handle_continue_1 = async (): Promise<void> => { + if (!map_geop || !map_geoc) + return void lc_gui_alert(`No farm location provided.`); //@todo + const farms_add_submission = schema_view_farms_add_submission.safeParse( + { + farm_name: val_farmname, + farm_area: val_farmarea ? parse_float(val_farmarea) : undefined, + farm_area_unit: + val_farmarea && val_farmarea_unit + ? val_farmarea_unit + : undefined, + farm_contact_name: val_farmcontact + ? val_farmcontact + : undefined, + geolocation_point: map_geop, + geocode_result: map_geoc, + } satisfies IViewFarmsAddSubmission, + ); + + if (!farms_add_submission.success) { + return void lc_gui_alert( + `Request invalid: ${farms_add_submission.error}`, + ); //@todo + } + await basis.on_submit({ payload: farms_add_submission.data }); + }; + + const handle_continue = async (): Promise<void> => { + switch ($casl_i) { + case 1: + return await handle_continue_1(); + default: + await casl_inc(); + } + }; + + const handle_back = async (): Promise<void> => { + switch ($casl_i) { + case 1: { + if (!geop_is_valid(map_geop)) { + const geop_cur = await lc_geop_current(); + if (geop_cur) { + map_geop = geop_cur; + const geoc_cur = await lc_geocode(geop_cur); + if (geoc_cur) map_geoc = geoc_cur; + focus_map_marker(); + } + } + } + default: + return await casl_dec(); + } + }; +</script> + +<LayoutView> + <PageToolbar + basis={{ + header: { + label: `${$ls(`common.farms`)} / ${`${$ls(`common.add`)}`}`, + callback_route: basis.callback_route, + }, + }} + > + {#snippet header_option()} + <!-- @todo {#if $casl_i === 0} + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + await handle_enter_location(); + }} + > + <p + class={`font-sans font-[600] text-[18px] text-ly0-gl-hl`} + > + {`${$ls(`common.enter_location`)}`} + </p> + <Glyph + basis={{ + classes: `text-ly0-gl-hl`, + dim: `md`, + key: `caret-right`, + }} + /> + </button> + {/if}--> + {/snippet} + </PageToolbar> + <Carousel> + <FarmsAddCasliMap + bind:map_geop + bind:map_geoc + {farm_geop_lat} + {farm_geop_lng} + /> + <FarmsAddCasliDetail + bind:val_farmname + bind:val_farmaddress + bind:val_farmcontact + bind:val_farmarea + bind:val_farmarea_unit + {farm_geop_lat} + {farm_geop_lng} + /> + </Carousel> +</LayoutView> +{#if $app_platform?.browser !== `safari`} + <LayoutBottomButton> + <ButtonLayoutPair + basis={{ + continue: { + label: `${$ls(`common.continue`)}`, + disabled: disabled_submit, + callback: handle_continue, + }, + back: { + label: `${$ls(`common.back`)}`, + visible: $casl_i > 0, + callback: handle_back, + }, + }} + /> + </LayoutBottomButton> +{/if} diff --git a/apps-lib-pwa/src/lib/views/farms/farms-details.svelte b/apps-lib-pwa/src/lib/views/farms/farms-details.svelte @@ -0,0 +1,247 @@ +<script lang="ts"> + import ButtonSimple from "$lib/components/buttons/button-simple.svelte"; + import LayoutPage from "$lib/components/layouts/layout-page.svelte"; + import LayoutView from "$lib/components/layouts/layout-view.svelte"; + import MapMarkerArea from "$lib/components/map/map-marker-area.svelte"; + import PageToolbar from "$lib/components/navigation/page-toolbar.svelte"; + import type { IViewBasis, IViewFarmsDetailsData } from "$lib/types/views"; + import { + Flex, + get_context, + Glyph, + type CallbackRoute, + } from "@radroots/apps-lib"; + import { + fmt_geolocation_address, + geol_lat_fmt, + geol_lng_fmt, + handle_err, + parse_geol_point_tup, + parse_tup_geop_point, + type CallbackPromiseGeneric, + type GeolocationPointTuple, + } from "@radroots/utils"; + import { onDestroy, onMount } from "svelte"; + import Map from "../../components/map/map.svelte"; + + const { ls, locale } = get_context(`lib`); + + let { + basis, + }: { + basis: IViewBasis<{ + data: IViewFarmsDetailsData; + callback_route?: CallbackRoute<string>; + on_handle_farm_lot_add: CallbackPromiseGeneric<string>; + on_handle_farm_products_view: CallbackPromiseGeneric<string>; + on_handle_farm_orders_view: CallbackPromiseGeneric<string>; + }>; + } = $props(); + + let map: maplibregl.Map | undefined = $state(undefined); + let map_center: GeolocationPointTuple = $state([0, 0]); + + onMount(async () => { + try { + if (basis.on_mount) await basis.on_mount(); + if (basis.data?.location) + map_center = parse_geol_point_tup(basis.data?.location.point); + if (map) { + map.setCenter(map_center); + map.setZoom(11); + } + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + onDestroy(async () => { + try { + if (basis.on_destroy) await basis.on_destroy(); + } catch (e) { + handle_err(e, `on_destroy`); + } + }); + + const map_geop = $derived(parse_tup_geop_point(map_center)); + + const farm_addr_fmt = $derived( + basis.data?.location?.address + ? fmt_geolocation_address(basis.data?.location.address) + : ``, + ); + + const farm_geop_lat = $derived( + basis.data?.location?.point + ? geol_lat_fmt(basis.data?.location.point.lat, `dms`, $locale, 3) + : ``, + ); + + const farm_geop_lng = $derived( + basis.data?.location?.point + ? geol_lng_fmt(basis.data?.location.point.lng, `dms`, $locale, 3) + : ``, + ); +</script> + +<LayoutView> + <PageToolbar + basis={{ + header: { + label: `${$ls(`common.farms`)}${basis.data?.farm.name ? ` / ${basis.data?.farm.name}` : ``}`, + callback_route: basis.callback_route, + }, + }} + /> + <LayoutPage> + <div + class={`flex flex-row h-[12rem] w-full justify-start items-center`} + > + <div + class={`flex flex-col basis-1/2 h-full p-4 gap-2 justify-start items-center`} + > + <div class={`flex flex-col w-full justify-start items-center`}> + <div + class={`flex flex-row w-full justify-start items-center`} + > + <p class={`font-sans font-[500] text-lg text-ly0-gl`}> + {farm_addr_fmt} + </p> + </div> + <div + class={`flex flex-row w-full justify-start items-center`} + > + <p + class={`font-sans font-[500] text-lg text-ly0-gl tracking-tight`} + > + {farm_geop_lat && farm_geop_lng + ? `${farm_geop_lat}, ${farm_geop_lng}` + : ``} + </p> + </div> + </div> + </div> + <div + class={`flex flex-col basis-1/2 h-full justify-start items-center`} + > + <div + class={`flex flex-col h-full w-full p-4 gap-4 justify-start items-center bg-ly1 rounded-2xl`} + > + <p class={`font-sans font-[500] text-sm text-ly0-gl`}> + {`Farm Info`} + </p> + <div + class={`flex flex-col w-full gap-1 justify-start items-center`} + > + <div + class={`flex flex-row w-full gap-4 justify-between items-center`} + > + <p class={`font-sans font-[400] text-ly0-gl`}> + {`Farm Size:`} + </p> + + {#if basis.data?.farm.area && basis.data?.farm.area_unit} + <p class={`font-sans font-[400] text-ly0-gl`}> + {`${basis.data?.farm.area} ${basis.data?.farm.area_unit}`} + </p> + {:else} + <div + class={`flex flex-row gap-line justify-start items-center`} + > + <p + class={`font-sans font-[400] text-ly0-gl_pl`} + > + {`Add`} + </p> + <Glyph + basis={{ + classes: `text-ly0-gl_pl`, + dim: `xs`, + key: `caret-right`, + }} + /> + </div> + {/if} + </div> + <div + class={`flex flex-row w-full gap-4 justify-between items-center`} + > + <p class={`font-sans font-[400] text-ly0-gl`}> + {`Farm Lots:`} + </p> + <p class={`font-sans font-[400] text-ly0-gl`}> + {`${basis.data?.lots?.length || 0}`} + </p> + </div> + <div + class={`flex flex-row w-full gap-4 justify-between items-center`} + > + <p class={`font-sans font-[400] text-ly0-gl`}> + {`Products:`} + </p> + <p class={`font-sans font-[400] text-ly0-gl`}> + {`${0}`} + </p> + </div> + <div + class={`flex flex-row w-full gap-4 justify-between items-center`} + > + <p class={`font-sans font-[400] text-ly0-gl`}> + {`Orders:`} + </p> + <p class={`font-sans font-[400] text-ly0-gl`}> + {`${0}`} + </p> + </div> + </div> + </div> + </div> + </div> + <div class={`flex flex-col w-full gap-3 justify-center items-center`}> + <ButtonSimple + basis={{ + label: `View Products`, + callback: async () => { + if (basis.data?.farm.id) + await basis.on_handle_farm_products_view( + basis.data?.farm.id, + ); + }, + }} + /> + <ButtonSimple + basis={{ + label: `View Orders`, + callback: async () => { + if (basis.data?.farm.id) + await basis.on_handle_farm_orders_view( + basis.data?.farm.id, + ); + }, + }} + /> + </div> + <div + class={`flex flex-col flex-shrink-0 h-[16rem] w-full justify-center items-center rounded-2xl overflow-hidden`} + > + <Map + bind:map + basis={{ + interactive: false, + }} + > + <MapMarkerArea + {map_geop} + basis={{ + no_drag: true, + }} + /> + </Map> + </div> + <div + class={`flex flex-col h-[12rem] w-full justify-center items-center`} + > + <Flex /> + </div> + </LayoutPage> +</LayoutView> diff --git a/apps-lib-pwa/src/lib/views/farms/farms.svelte b/apps-lib-pwa/src/lib/views/farms/farms.svelte @@ -0,0 +1,84 @@ +<script lang="ts"> + import ButtonLabelDashed from "$lib/components/buttons/button-label-dashed.svelte"; + import GlyphButtonSimple from "$lib/components/buttons/glyph-button-simple.svelte"; + import LayoutPage from "$lib/components/layouts/layout-page.svelte"; + import LayoutView from "$lib/components/layouts/layout-view.svelte"; + import PageToolbar from "$lib/components/navigation/page-toolbar.svelte"; + import FarmsDisplayLiEl from "$lib/features/farm/farms-display-li-el.svelte"; + import type { IViewBasis, IViewFarmsData } from "$lib/types/views"; + import { Fade, get_context, idb_init_page, type CallbackRoute } from "@radroots/apps-lib"; + import { + handle_err, + type CallbackPromise, + type CallbackPromiseGeneric, + } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { ls } = get_context(`lib`); + + let { + basis, + }: { + basis: IViewBasis<{ + data?: IViewFarmsData; + callback_route?: CallbackRoute<string>; + on_handle_farm_add: CallbackPromise; + on_handle_farm_view: CallbackPromiseGeneric<string>; + }>; + } = $props(); + + onMount(async () => { + try { + if (!basis.kv_init_prevent) await idb_init_page(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); +</script> + +<LayoutView> + <PageToolbar + basis={{ + header: { + label: `${$ls(`common.farms`)}`, + callback_route: basis.callback_route, + }, + }} + > + {#snippet header_option()} + {#if basis.data?.list.length} + <Fade> + <GlyphButtonSimple + basis={{ + label: `${$ls(`icu.add_*`, { value: `${$ls(`common.farm`)}` })}`, + callback: async () => { + await basis.on_handle_farm_add(); + }, + }} + /> + </Fade> + {/if} + {/snippet} + </PageToolbar> + <LayoutPage> + {#if basis.data} + {#if basis.data?.list.length} + {#each basis.data?.list || [] as li} + <FarmsDisplayLiEl + basis={li} + on_handle_farm_view={basis.on_handle_farm_view} + /> + {/each} + {:else} + <ButtonLabelDashed + basis={{ + label: `Add farm`, + callback: async () => { + await basis.on_handle_farm_add(); + }, + }} + /> + {/if} + {/if} + </LayoutPage> +</LayoutView> diff --git a/apps-lib-pwa/src/lib/views/home.svelte b/apps-lib-pwa/src/lib/views/home.svelte @@ -0,0 +1,56 @@ +<script lang="ts"> + import ButtonSimple from "$lib/components/buttons/button-simple.svelte"; + import LayoutPage from "$lib/components/layouts/layout-page.svelte"; + import LayoutView from "$lib/components/layouts/layout-view.svelte"; + import NavigationTabs from "$lib/components/navigation/navigation-tabs.svelte"; + import PageToolbar from "$lib/components/navigation/page-toolbar.svelte"; + import type { IViewBasis, IViewHomeData } from "$lib/types/views"; + import { get_context, idb_init_page } from "@radroots/apps-lib"; + + import { handle_err, type CallbackPromise } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { ls } = get_context(`lib`); + + let { + basis, + }: { + basis: IViewBasis<{ + data?: IViewHomeData; + on_handle_farms: CallbackPromise; + on_handle_products: CallbackPromise; + }>; + } = $props(); + + onMount(async () => { + try { + if (!basis.kv_init_prevent) await idb_init_page(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); +</script> + +{#if basis.data} + {@const { data: basis_data } = basis} + <LayoutView> + <PageToolbar + basis={{ + header: { + label: `${$ls(`common.general`)}`, + }, + }} + /> + <LayoutPage> + <ButtonSimple + basis={{ + label: `${$ls(`common.farms`)}`, + callback: async () => { + await basis.on_handle_farms(); + }, + }} + /> + </LayoutPage> + </LayoutView> + <NavigationTabs /> +{/if} diff --git a/apps-lib-pwa/src/lib/views/profile/profile.svelte b/apps-lib-pwa/src/lib/views/profile/profile.svelte @@ -0,0 +1,305 @@ +<script lang="ts"> + import ButtonRoundNav from "$lib/components/buttons/button-round-nav.svelte"; + import FloatPage from "$lib/components/lib/float-page.svelte"; + import ImageUploadAddPhoto from "$lib/components/lib/image-upload-add-photo.svelte"; + import SelectMenu from "$lib/components/lib/select-menu.svelte"; + import NavigationTabs from "$lib/components/navigation/navigation-tabs.svelte"; + import type { IViewBasis } from "$lib/types/views"; + import { + get_context, + Glyph, + idb_init_page, + ImagePath, + symbols, + type IViewOnDestroy, + } from "@radroots/apps-lib"; + import { + handle_err, + type CallbackPromise, + type CallbackPromiseGeneric, + } from "@radroots/utils"; + import { onDestroy, onMount } from "svelte"; + import type { IViewProfileData, ViewProfileEditFieldKey } from "./types"; + + const { ls } = get_context(`lib`); + + let { + basis, + photo_path = $bindable(``), + }: { + basis: IViewBasis<{ + data?: IViewProfileData; + loading_photo_upload: boolean; + loading_photo_upload_open: boolean; + on_handle_back: CallbackPromiseGeneric<{ + is_photo_existing: boolean; + }>; + on_handle_photo_options: CallbackPromise; + on_handle_edit_profile_field: CallbackPromiseGeneric<{ + field: ViewProfileEditFieldKey; + }>; + }> & + IViewOnDestroy<{ public_key: string }>; + photo_path: string; + } = $props(); + + type ViewDisplay = `photos` | `following` | `followers`; + let view_display: ViewDisplay = $state(`photos`); + + let val_sel_options_button = $state(``); + + onMount(async () => { + try { + if (!basis.kv_init_prevent) await idb_init_page(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); + + onDestroy(async () => { + try { + if (basis.data?.profile.public_key) + await basis.on_destroy({ + public_key: basis.data?.profile.public_key, + }); + } catch (e) { + handle_err(e, `on_destroy`); + } + }); + + const photo_overlay_visible = $derived( + !!(basis.data?.profile.picture || photo_path), + ); + + const classes_photo_overlay_glyph = $derived( + photo_overlay_visible ? `text-white` : `text-ly0-gl`, + ); + + const classes_photo_overlay_glyph_opt = $derived( + photo_overlay_visible ? `text-gray-300` : `text-ly0-gl`, + ); + + const classes_photo_overlay_glyph_opt_selected = $derived( + photo_overlay_visible ? `text-white` : `text-ly1-gl_d`, + ); +</script> + +{#if basis.data} + <div + class={`relative flex flex-col min-h-[525px] h-[525px] w-full justify-center items-center bg-ly2 fade-in`} + > + <FloatPage + basis={{ + posx: `left`, + }} + > + <ButtonRoundNav + basis={{ + glyph: `arrow-left`, + loading: basis.loading_photo_upload, + callback: async () => { + await basis.on_handle_back({ + is_photo_existing: photo_overlay_visible, + }); + }, + }} + /> + </FloatPage> + <FloatPage + basis={{ + posx: `right`, + }} + > + <SelectMenu + bind:value={val_sel_options_button} + basis={{ + layer: 0, + options: [ + { + entries: [ + { + value: `*add-new`, + label: `Add new photo`, + }, + ], + }, + ], + }} + > + <ButtonRoundNav + basis={{ + glyph: `images-square`, + callback: basis.on_handle_photo_options, + }} + /> + </SelectMenu> + </FloatPage> + {#if basis.data?.profile.picture || photo_path} + {@const img_path = photo_path || basis.data?.profile.picture || ``} + <ImagePath basis={{ path: img_path }} /> + {:else} + <div + class={`flex flex-row justify-start items-center -translate-y-8`} + > + <ImageUploadAddPhoto + bind:photo_path + basis={{ + loading: basis.loading_photo_upload_open, + }} + /> + </div> + {/if} + <div + class={`absolute bottom-0 left-0 flex flex-col h-[calc(100%-100%/1.618)] w-full px-6 gap-2 justify-end items-center`} + > + <div + class={`flex flex-col w-full gap-[2px] justify-center items-center`} + > + <div + class={`flex flex-row h-10 w-full justify-start items-center`} + > + <button + class={`group flex flex-row justify-center items-center`} + onclick={async () => { + await basis.on_handle_edit_profile_field({ + field: `display_name`, + }); + }} + > + <p + class={`font-sansd font-[600] text-[2rem] ${classes_photo_overlay_glyph} ${basis.data?.profile.name ? `` : `capitalize opacity-active`} el-re`} + > + {#if basis.data?.profile.display_name} + {`${basis.data?.profile.display_name}`} + {:else if basis.data?.profile.name} + {`${basis.data?.profile.display_name || basis.data?.profile.name || ``}`} + {:else} + {`+ ${`${$ls(`icu.add_*`, { value: `${$ls(`common.profile_name`)}` })}`}`} + {/if} + </p> + </button> + </div> + <div + class={`flex flex-row w-full gap-[6px] justify-start items-center`} + > + <button + class={`group flex flex-row justify-center items-center`} + onclick={async () => { + await basis.on_handle_edit_profile_field({ + field: `name`, + }); + }} + > + <p + class={`font-sansd font-[600] text-[1.1rem] ${classes_photo_overlay_glyph} ${basis.data?.profile.name ? `` : `capitalize opacity-active`} el-re`} + > + {#if basis.data?.profile.name} + {`@${basis.data?.profile.name}`} + {:else} + {`+ ${`${$ls(`icu.add_*`, { value: `${$ls(`common.username`)}` })}`}`} + {/if} + </p> + </button> + <p + class={`font-sans font-[400] ${classes_photo_overlay_glyph}`} + > + {symbols.bullet} + </p> + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + alert(`@todo!`); + }} + > + <Glyph + basis={{ + classes: `${classes_photo_overlay_glyph}`, + dim: `xs`, + + key: `link-simple`, + }} + /> + </button> + </div> + <div class={`flex flex-row w-full justify-start items-center`}> + <button + class={`group flex flex-row justify-center items-center`} + onclick={async () => { + await basis.on_handle_edit_profile_field({ + field: `about`, + }); + }} + > + <p + class={`font-sansd font-[400] text-[1.1rem] ${classes_photo_overlay_glyph} ${basis.data?.profile.about ? `` : `capitalize opacity-active`}`} + > + {#if basis.data?.profile.about} + {`${basis.data?.profile.about}`} + {:else} + {`+ ${`${$ls(`icu.add_*`, { value: `${$ls(`common.bio`)}` })}`}`} + {/if} + </p> + </button> + </div> + </div> + <div + class={`flex flex-row w-full pt-2 pb-6 gap-2 justify-start items-center`} + > + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + view_display = `photos`; + }} + > + <p + class={`font-sans text-[1.1rem] font-[600] capitalize ${view_display === `photos` ? classes_photo_overlay_glyph_opt_selected : classes_photo_overlay_glyph_opt} el-re`} + > + {`${$ls(`common.photos`)}`} + </p> + </button> + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + view_display = `following`; + }} + > + <p + class={`font-sans text-[1.1rem] font-[600] capitalize ${view_display === `following` ? classes_photo_overlay_glyph_opt_selected : classes_photo_overlay_glyph_opt} el-re`} + > + {`${$ls(`common.following`)}`} + </p> + </button> + <button + class={`flex flex-row justify-center items-center`} + onclick={async () => { + view_display = `followers`; + }} + > + <p + class={`font-sans text-[1.1rem] font-[600] capitalize ${view_display === `followers` ? classes_photo_overlay_glyph_opt_selected : classes_photo_overlay_glyph_opt} el-re`} + > + {`${$ls(`common.followers`)}`} + </p> + </button> + </div> + </div> + </div> + <div + class={`flex flex-col w-full min-h-[500px] justify-start items-center`} + > + {#if view_display === `photos`} + <p class={`font-sans font-[400] text-ly0-gl`}> + {view_display} + </p> + {:else if view_display === `following`} + <p class={`font-sans font-[400] text-ly0-gl`}> + {view_display} + </p> + {:else if view_display === `followers`} + <p class={`font-sans font-[400] text-ly0-gl`}> + {view_display} + </p> + {/if} + </div> + <NavigationTabs /> +{/if} diff --git a/apps-lib-pwa/src/lib/views/profile/types.ts b/apps-lib-pwa/src/lib/views/profile/types.ts @@ -0,0 +1,12 @@ +import type { NostrProfile } from "@radroots/tangle-schema-bindings"; + +export type IViewProfileData = { + profile: NostrProfile +}; + +export type ViewProfileEditFieldKey = `name` | `display_name` | `about`; + +export type IViewProfileEditData = { + public_key: string; + field: ViewProfileEditFieldKey; +}; diff --git a/apps-lib-pwa/src/lib/views/settings.svelte b/apps-lib-pwa/src/lib/views/settings.svelte @@ -0,0 +1,109 @@ +<script lang="ts"> + import LayoutTrellis from "$lib/components/layouts/layout-trellis.svelte"; + import LayoutView from "$lib/components/layouts/layout-view.svelte"; + import PageToolbar from "$lib/components/navigation/page-toolbar.svelte"; + import Trellis from "$lib/components/trellis/trellis.svelte"; + import type { ITrellisKind } from "$lib/types/components/trellis"; + import type { IViewBasis } from "$lib/types/views"; + import { + get_context, + idb_init_page, + symbols, + theme_mode, + } from "@radroots/apps-lib"; + import { handle_err } from "@radroots/utils"; + import { onMount } from "svelte"; + + const { ls, lc_color_mode } = get_context(`lib`); + + let { + basis, + }: { + basis: IViewBasis<{ + trellis_2?: (ITrellisKind | undefined)[]; + }>; + } = $props(); + + onMount(async () => { + try { + if (!basis.kv_init_prevent) await idb_init_page(); + } catch (e) { + handle_err(e, `on_mount`); + } + }); +</script> + +<LayoutView> + <PageToolbar + basis={{ + header: { + label: `${$ls(`common.settings`)}`, + }, + }} + /> + <LayoutTrellis> + <Trellis + basis={{ + layer: 1, + title: { + value: `Appearance`, + }, + list: [ + { + hide_active: true, + select: { + label: { + left: [ + { + value: `${$ls(`common.color_mode`)}`, + classes: `capitalize`, + }, + ], + }, + display: { + label: { + value: `${$theme_mode}`, + classes: `capitalize`, + }, + }, + el: { + value: $theme_mode, + options: [ + { + entries: [ + { + value: symbols.bullet, + label: `${$ls(`icu.choose_*`, { value: `${$ls(`common.color_mode`)}`.toLowerCase() })}`, + disabled: true, + }, + { + value: `light`, + label: `${$ls(`common.light`)}`, + }, + { + value: `dark`, + label: `${$ls(`common.dark`)}`, + }, + ], + }, + ], + callback: lc_color_mode, + }, + end: { + glyph: { + key: `caret-right`, + }, + }, + }, + }, + ], + }} + /> + <Trellis + basis={{ + layer: 1, + list: basis.trellis_2, + }} + /> + </LayoutTrellis> +</LayoutView> diff --git a/apps-lib-pwa/vite.config.ts b/apps-lib-pwa/vite.config.ts @@ -2,5 +2,5 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit() as any] });