web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit a4a8d92fcd9f1ce4ed3e5a11a1d1cf4fc8e42fc2
parent 4ca507394f753cdb2622c35ce55992690040eba0
Author: triesap <triesap@radroots.dev>
Date:   Mon, 17 Nov 2025 23:51:11 +0000

apps-lib: add themed ui primitives, app/carousel state, and browser/idb/geo utilities, consolidating exports and ignore rules

Diffstat:
Mapps-lib/.gitignore | 54++++++++++++++++++++++++++----------------------------
Dapps-lib/README.md | 1-
Mapps-lib/package.json | 1+
Aapps-lib/src/lib/components/carousel-item.svelte | 19+++++++++++++++++++
Aapps-lib/src/lib/components/carousel.svelte | 25+++++++++++++++++++++++++
Aapps-lib/src/lib/components/fade.svelte | 34++++++++++++++++++++++++++++++++++
Mapps-lib/src/lib/components/glyph.svelte | 26+++++++++++++-------------
Capps-lib/src/lib/components/glyph.svelte -> apps-lib/src/lib/components/glyphi.svelte | 0
Aapps-lib/src/lib/components/image-blob.svelte | 31+++++++++++++++++++++++++++++++
Aapps-lib/src/lib/components/image-path.svelte | 23+++++++++++++++++++++++
Aapps-lib/src/lib/components/image-src.svelte | 18++++++++++++++++++
Mapps-lib/src/lib/index.ts | 22++++++++++++++++++----
Aapps-lib/src/lib/stores/app.ts | 4++++
Aapps-lib/src/lib/stores/carousel.ts | 39+++++++++++++++++++++++++++++++++++++++
Mapps-lib/src/lib/stores/theme.ts | 4++--
Aapps-lib/src/lib/styles/glyphs.ts | 19+++++++++++++++++++
Mapps-lib/src/lib/types/components.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mapps-lib/src/lib/types/lib.ts | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Aapps-lib/src/lib/types/ui.ts | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib/src/lib/utils/app/carousel.ts | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib/src/lib/utils/browser.ts | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib/src/lib/utils/geo.ts | 8++++++++
Aapps-lib/src/lib/utils/idb/kv.ts | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dapps-lib/src/lib/utils/idb/lib.ts | 86-------------------------------------------------------------------------------
Mapps-lib/src/lib/utils/lib.ts | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mapps-lib/vite.config.ts | 2+-
26 files changed, 1142 insertions(+), 145 deletions(-)

diff --git a/apps-lib/.gitignore b/apps-lib/.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/README.md b/apps-lib/README.md @@ -1 +0,0 @@ -# lib-app diff --git a/apps-lib/package.json b/apps-lib/package.json @@ -51,6 +51,7 @@ "@radroots/locales": "*", "@radroots/utils": "*", "@radroots/utils-nostr": "*", + "@radroots/themes": "*", "@nostr-dev-kit/ndk": "2.14.33", "@nostr-dev-kit/ndk-cache-dexie": "2.6.34", "@nostr-dev-kit/ndk-svelte": "2.4.38", diff --git a/apps-lib/src/lib/components/carousel-item.svelte b/apps-lib/src/lib/components/carousel-item.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + import type { IBasisOpt, IClOpt } from "$lib/types/ui"; + import { fmt_cl } from "$lib/utils/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 flex-shrink-0 w-[100vw] justify-start items-center`} +> + {@render children()} +</div> diff --git a/apps-lib/src/lib/components/carousel.svelte b/apps-lib/src/lib/components/carousel.svelte @@ -0,0 +1,25 @@ +<script lang="ts"> + import { casl_i } from "$lib/stores/carousel"; + import type { IBasisOpt, IClOpt } from "$lib/types/ui"; + import { fmt_cl } from "$lib/utils/lib"; + import type { Snippet } from "svelte"; + + let { + basis = undefined, + children, + }: { + basis?: IBasisOpt<IClOpt>; + children: Snippet; + } = $props(); +</script> + +<div + class={`${fmt_cl(basis?.classes)} relative flex flex-col w-full overflow-hidden`} +> + <div + class={`flex h-full transition-transform duration-500`} + style={`transform: translateX(-${Math.max($casl_i, 0) * 100}vw)`} + > + {@render children()} + </div> +</div> diff --git a/apps-lib/src/lib/components/fade.svelte b/apps-lib/src/lib/components/fade.svelte @@ -0,0 +1,34 @@ +<script lang="ts"> + import { fmt_cl } from "$lib/utils/lib"; + import type { Snippet } from "svelte"; + import { cubicIn, cubicOut } from "svelte/easing"; + import { fade, type FadeParams } from "svelte/transition"; + + let { + basis, + children, + }: { + basis?: { + in?: FadeParams; + out?: FadeParams; + classes?: string; + }; + children: Snippet; + } = $props(); +</script> + +<div + in:fade={{ + duration: 200, + easing: cubicIn, + ...basis?.in, + }} + out:fade={{ + duration: 200, + easing: cubicOut, + ...basis?.out, + }} + class={`flex w-full overflow-hidden ${fmt_cl(basis?.classes)}`} +> + {@render children()} +</div> diff --git a/apps-lib/src/lib/components/glyph.svelte b/apps-lib/src/lib/components/glyph.svelte @@ -1,20 +1,20 @@ <script lang="ts"> - import type { IGlyphI } from "$lib/types/components"; + import { glyph_style_map } from "$lib/styles/glyphs"; + import type { IGlyph } from "$lib/types/components"; import { fmt_cl } from "$lib/utils/lib"; - const styles = { - xs: `text-[16px]`, - sm: `text-[20px]`, - md: `text-[26px]`, - lg: `text-[32px]`, - xl: `text-[48px]`, - "2xl": "text-[64px]", - }; + let { basis }: { basis: IGlyph } = $props(); - let { basis }: { basis: IGlyphI } = $props(); + const styles = $derived( + basis?.dim ? glyph_style_map.get(basis.dim) : glyph_style_map.get(`sm`), + ); + + const weight = $derived(basis.weight ? `-${basis.weight}` : `-bold`); </script> -<i - class={`ph${!basis?.weight || basis?.weight === `regular` ? `` : `-${basis?.weight}`} ph-${basis.key} ${fmt_cl(basis.classes)} ${basis.size ? styles[basis.size] : ""}`} +<div + id={basis.id || null} + class={`${fmt_cl(basis.classes)} flex flex-row text-[${styles?.gl_1}px] justify-center items-center`} > -</i> + <i class={`ph${weight} ph-${basis.key}`}></i> +</div> diff --git a/apps-lib/src/lib/components/glyph.svelte b/apps-lib/src/lib/components/glyphi.svelte diff --git a/apps-lib/src/lib/components/image-blob.svelte b/apps-lib/src/lib/components/image-blob.svelte @@ -0,0 +1,31 @@ +<script lang="ts"> + import type { IImageBlob } from "$lib/types/ui"; + import { fmt_cl, to_arr_buf } from "$lib/utils/lib"; + + let { basis }: { basis: IImageBlob } = $props(); + + const img_src = $derived( + basis.data + ? URL.createObjectURL( + new Blob( + [ + basis.data instanceof Uint8Array + ? to_arr_buf(basis.data) + : basis.data, + ], + { type: "image/jpeg" }, + ), + ) + : undefined, + ); +</script> + +{#if img_src} + <img + id={basis?.id || null} + class={`${fmt_cl(basis?.classes)}`} + src={img_src} + alt={basis?.alt || null} + style="height: 100%; width: 100%; object-fit: cover; display: block;" + /> +{/if} diff --git a/apps-lib/src/lib/components/image-path.svelte b/apps-lib/src/lib/components/image-path.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import type { IImagePath } from "$lib/types/ui"; + import { get_context } from "$lib/utils/lib"; + import ImageBlob from "./image-blob.svelte"; + import ImageSrc from "./image-src.svelte"; + + const { lc_img_bin } = get_context(`lib`); + let { basis }: { basis: IImagePath } = $props(); + + const img_path = $derived(basis.path); +</script> + +{#if img_path} + {@const is_bin = img_path.startsWith(`file:`)} + + {#if is_bin} + {#await lc_img_bin(img_path) then data} + <ImageBlob basis={{ data, ...basis }} /> + {/await} + {:else} + <ImageSrc basis={{ src: img_path, ...basis }} /> + {/if} +{/if} diff --git a/apps-lib/src/lib/components/image-src.svelte b/apps-lib/src/lib/components/image-src.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import type { IImageSource } from "$lib/types/ui"; + import { fmt_cl } from "$lib/utils/lib"; + + let { basis }: { basis: IImageSource } = $props(); + + const img_src = $derived(basis.src); +</script> + +{#if img_src} + <img + id={basis?.id || null} + class={`${fmt_cl(basis?.classes)}`} + src={img_src || null} + alt={basis?.alt || null} + style={`height: 100%; width: 100%; object-fit: cover; display: block;`} + /> +{/if} diff --git a/apps-lib/src/lib/index.ts b/apps-lib/src/lib/index.ts @@ -1,13 +1,27 @@ -export { default as Flex } from "./components/flex.svelte" -export { default as Glyph } from "./components/glyph.svelte" -export { default as Input } from "./components/input.svelte" +export * from "./stores/app.js" +export * from "./stores/carousel.js" export * from "./stores/ndk.js" export * from "./stores/theme.js" +export * from "./styles/glyphs.js" export * from "./types/components.js" export * from "./types/lib.js" export * from "./types/ndk.js" +export * from "./types/ui.js" +export * from "./utils/app/carousel.js" +export * from "./utils/browser.js" export * from "./utils/fetch/lib.js" +export * from "./utils/geo.js" export * from "./utils/i18n.js" -export * from "./utils/idb/lib.js" +export * from "./utils/idb/kv.js" export * from "./utils/lib.js" export * from "./utils/nostr/lib.js" +export { default as Carousel } from "./components/carousel.svelte" +export { default as CarouselItem } from "./components/carousel-item.svelte" +export { default as Fade } from "./components/fade.svelte" +export { default as Flex } from "./components/flex.svelte" +export { default as Glyph } from "./components/glyph.svelte" +export { default as Glyphi } from "./components/glyphi.svelte" +export { default as ImageBlob } from "./components/image-blob.svelte" +export { default as ImagePath } from "./components/image-path.svelte" +export { default as ImageSrc } from "./components/image-src.svelte" +export { default as Input } from "./components/input.svelte" diff --git a/apps-lib/src/lib/stores/app.ts b/apps-lib/src/lib/stores/app.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; + +export const win_h = writable<number>(0); +export const win_w = writable<number>(0); diff --git a/apps-lib/src/lib/stores/carousel.ts b/apps-lib/src/lib/stores/carousel.ts @@ -0,0 +1,39 @@ +import { get_store } from "$lib/utils/lib"; +import { writable } from "svelte/store"; + +export const carousel_active = writable<boolean>(false); +export const casl_i = writable<number>(0); +export const casl_imax = writable<number>(0); + +const fn_carousel_num = (num_i: number, num_min: number) => { + const store = writable<number>(num_i); + return { + subscribe: store.subscribe, + set: (num: number) => { + store.set(Math.max(num, num_min)); + }, + update: (updater: (num: number) => number) => { + store.update((num) => Math.max(updater(num), num_min)); + } + }; +} +export const carousel_num = fn_carousel_num(1, 1); + +export const casl_inc = async (opts?: 'noflow'): Promise<void> => { + const $casl_i = get_store(casl_i); + const $casl_imax = get_store(casl_imax); + if (opts === 'noflow' && $casl_i < $casl_imax) casl_i.set($casl_i + 1); + else casl_i.set(($casl_i + 1) % ($casl_imax + 1)); +}; + +export const casl_dec = async (opts?: 'noflow'): Promise<void> => { + const $casl_i = get_store(casl_i); + const $casl_imax = get_store(casl_imax); + if (opts === 'noflow' && $casl_i > 0) casl_i.set($casl_i - 1); + else casl_i.set(($casl_i - 1 + ($casl_imax + 1)) % ($casl_imax + 1)); +}; + +export const casl_init = (index_curr: number, index_max: number): void => { + casl_i.set(index_curr); + casl_imax.set(index_max); +}; diff --git a/apps-lib/src/lib/stores/theme.ts b/apps-lib/src/lib/stores/theme.ts @@ -1,10 +1,10 @@ -import type { ThemeMode } from "$lib/types/lib"; import { get_store } from "$lib/utils/lib"; +import type { ThemeKey, ThemeMode } from "@radroots/themes"; import { type CallbackPromiseGeneric } from "@radroots/utils"; import { writable } from "svelte/store"; export const theme_mode = writable<ThemeMode>(); -export const theme_key = writable<string>(); +export const theme_key = writable<ThemeKey>(); export const theme_reset = writable<boolean>(false); export const theme_toggle = async (callback: CallbackPromiseGeneric<ThemeMode>): Promise<void> => { diff --git a/apps-lib/src/lib/styles/glyphs.ts b/apps-lib/src/lib/styles/glyphs.ts @@ -0,0 +1,18 @@ +import type { GeometryGlyphDimension } from "$lib/types/lib"; + +export const glyph_style_map: Map<GeometryGlyphDimension, { gl_1: number; dim_1?: number; }> = new Map([ + ["xs--", { gl_1: 12 }], + ["xs-", { gl_1: 12, dim_1: 17 }], + ["xs", { gl_1: 15, dim_1: 18 }], + ["xs+", { gl_1: 18, dim_1: 20 }], + ["sm-", { gl_1: 19, dim_1: 22 }], + ["sm", { gl_1: 20, dim_1: 24 }], + ["sm+", { gl_1: 21 }], + ["md-", { gl_1: 23 }], + ["md", { gl_1: 24 }], + ["md+", { gl_1: 26 }], + ["lg-", { gl_1: 27 }], + ["lg", { gl_1: 28 }], + ["xl", { gl_1: 30 }], + ["xl+", { gl_1: 40 }], +]); +\ No newline at end of file diff --git a/apps-lib/src/lib/types/components.ts b/apps-lib/src/lib/types/components.ts @@ -1,7 +1,9 @@ -import type { CallbackPromiseArgs } from "$lib"; +import type { ElementCallbackMount, ElementCallbackValue, ElementCallbackValueBlur, ElementCallbackValueKeydown, GeometryGlyphDimension, GlyphKey, GlyphWeight, ICbOpt, IClOpt, IconWeight, IDisabledOpt, IIdGOpt, IIdOpt, ILyOpt } from "$lib"; +import type { CallbackPromiseGeneric, FormField } from "@radroots/utils"; import type { HTMLInputTypeAttribute } from "svelte/elements"; -export type IconWeight = `regular` | `bold` | `fill`; + +export type EntryStyle = `guide` | `line`; export type IGlyphI = { classes?: string; @@ -10,6 +12,12 @@ export type IGlyphI = { size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"; }; +export type IGlyph = ICbOpt & IIdOpt & ILyOpt & IClOpt & { + weight?: GlyphWeight; + key: GlyphKey; + dim?: GeometryGlyphDimension; +}; + export type ILibInputKeydown = { key: string; is_submit: boolean; @@ -22,6 +30,53 @@ export type ILibInput = { pattern?: string; placeholder?: string; disabled?: boolean; - callback_keydown?: CallbackPromiseArgs<ILibInputKeydown>; - callback_input?: CallbackPromiseArgs<HTMLInputElement>; + callback_keydown?: CallbackPromiseGeneric<ILibInputKeydown>; + callback_input?: CallbackPromiseGeneric<HTMLInputElement>; +}; + +export type IInput<T extends string> = IIdGOpt<T> & IClOpt & ILyOpt & IDisabledOpt & { + placeholder?: string; + label?: string; + hidden?: boolean; + validate?: RegExp; + sync?: boolean; + field?: FormField; + field_constrain?: boolean; + callback?: ElementCallbackValue, + callback_keydown?: ElementCallbackValueKeydown<HTMLInputElement>, + callback_blur?: ElementCallbackValueBlur<HTMLInputElement>; + callback_focus?: ElementCallbackValueBlur<HTMLInputElement>; + callback_mount?: ElementCallbackMount<HTMLInputElement>; +}; + +export type IInputValue<T extends string> = Omit<IInput<T>, `sync`>; + +export type ISelectOption<T extends string> = IDisabledOpt & { + value: T; + label: string; +}; + +export type ISelectCallback = CallbackPromiseGeneric<ISelectOption<string>> + +export type ISelect = IIdOpt & IClOpt & ILyOpt & { + callback?: ISelectCallback; + sync?: boolean; + sync_init?: boolean; + options: { group?: string | true; entries: ISelectOption<string>[] }[]; + show_arrows?: 'l' | 'r'; +}; + +export type ITextArea = IIdOpt & IClOpt & ILyOpt & { + placeholder?: string; + label?: string; + hidden?: boolean; + validate?: RegExp; + sync?: true; + field?: FormField; + field_constrain?: boolean; + callback?: ElementCallbackValue, + callback_keydown?: ElementCallbackValueKeydown<HTMLTextAreaElement>, + callback_blur?: ElementCallbackValueBlur<HTMLTextAreaElement>; + callback_focus?: ElementCallbackValueBlur<HTMLTextAreaElement>; + callback_mount?: ElementCallbackMount<HTMLTextAreaElement>; }; \ No newline at end of file diff --git a/apps-lib/src/lib/types/lib.ts b/apps-lib/src/lib/types/lib.ts @@ -1,7 +1,197 @@ +import type { CallbackPromise, CallbackPromiseFull, CallbackPromiseGeneric, CallbackPromiseResult, GeocoderReverseResult, GeolocationPoint, IClientGeolocationPosition, MediaImageUploadResult } from "@radroots/utils"; import type { Writable } from "svelte/store"; +import type { ISelectOption } from "./components"; -export type ThemeMode = 'light' | 'dark'; export type StoreWritable<S> = S extends Writable<infer T> ? T : never; -export type CallbackPromise = () => Promise<void> -export type CallbackPromiseArgs<T> = (args: T) => Promise<void> +export type INavigationRoute<T extends string> = { + route: T | [T, NavigationParamTuple<NavigationRouteParamKey>[]]; +}; +export type CallbackRoute<T extends string> = CallbackPromise | INavigationRoute<T>; + +export type IViewOnMount<TypeCallbackParam> = { + on_mount: CallbackPromiseGeneric<TypeCallbackParam>; +}; + +export type IViewOnDestroy<TypeCallbackParam> = { + on_destroy: CallbackPromiseGeneric<TypeCallbackParam>; +}; + + +export type IconWeight = `regular` | `bold` | `fill`; + +export type GlyphKey = | + `video-camera` | + `device-mobile-camera` | + `crop` | + `map-trifold` | + `trash-simple` | + `backspace` | + `user-circle-check` | + `images-square` | + `bell` | + `columns` | + `bold` | + `article` | + `grid-four` | + `link-simple` | + `seal-check` | + `selection-foreground` | + `image-square` | + `image-broken` | + `funnel` | + `users-three` | + `note-blank` | + `user-circle-plus` | + `user-circle` | + `receipt` | + `invoice` | + `note` | + `arrow-left` | + `arrows-down-up` | + `basket` | + `arrow-right` | + `upload-simple` | + `printer` | + `download-simple` | + `list` | + `asterisk` | + `asterisk-simple` | + `subtitles-slash` | + `cardholder` | + `globe-x` | + `exclamation-mark` | + `network-x` | + `x-circle` | + `address-book-tabs` | + `paper-plane-tilt` | + `note-pencil` | + `share-fat` | + `folder` | + `trash` | + `plus-circle` | + `currency-${GlyphKeyCurrency}` | + `arrow-down` | + `caret-circle-down` | + `caret-circle-up` | + `shopping-bag-open` | + `coffee-bean` | + `compass` | + `map-pin-simple` | + `handbag-simple` | + `devices` | + `lock-key` | + `gear` | + `gear-fine` | + `bell-simple` | + `envelope` | + `house-line` | + `arrows-left-right` | + `list-plus` | + `squares-four` | + `list-plus` | + `app-window` | + `circle-notch` | + `subtract-square` | + `device-tablet-speaker` | + `weather-cloud` | + `warning` | + `circle-notch` | + `minus` | + `key` | + `arrow-u-up-left` | + `arrow-counter-clockwise` | + `circle` | + `check-circle` | + `circle-dashed` | + `dots-three` | + `cards-three` | + `lightning` | + `cards` | + `note-pencil` | + `tray` | + `calendar-dots` | + `notepad` | + `network` | + `calendar-blank` | + `chats-circle` | + `plant` | + `farm` | + `magnifying-glass` | + `chat-circle-dots` | + `dots-three-outline` | + `copy` | + `circles-four` | + `waveform` | + `film-strip` | + `arrow-up` | + `arrow-circle-up` | + `plus` | + `funnel-simple` | + `user` | + `camera` | + `check` | + `file` | + `share-network` | + `question` | + `minus-circle` | + `globe-simple` | + `globe` | + `warning-circle` | + `x` | + `info` | + `caret-${GeometryCardinalDirection}` | + `caret-up-down`; + +export type GlyphKeyCurrency = `dollar` | `eur`; +export type GlyphWeight = `bold` | `fill` + +export type GeometryScreenPositionHorizontal = `left` | `center` | `right`; +export type GeometryScreenPositionVertical = `top` | `center` | `bottom`; +export type GeometryScreenPosition = `${GeometryScreenPositionVertical}-${GeometryScreenPositionHorizontal}`; +export type GeometryCardinalDirection = `up` | `down` | `left` | `right`; + +export type GeometryDimension = + `xs` | + `sm` | + `md` | + `lg` | + `xl`; +export type GeometryGlyphDimension = + | `${GeometryDimension}` + | `${GeometryDimension}-` + | `${GeometryDimension}--` + | `${GeometryDimension}+`; + +export type LayerGlyphBasisKind = `_a` | `_d` | `_pl`; + +export type LoadingBlades = 8 | 12; +export type LoadingDimension = GeometryDimension | `glyph-send-button`; //@todo remove + +export type ElementCallbackValue = CallbackPromiseGeneric<{ value: string; pass: boolean; }>; +export type ElementCallbackValueKeydown<T extends HTMLElement> = CallbackPromiseGeneric<{ key: string; key_s: boolean; el: T }>; +export type ElementCallbackValueBlur<T extends HTMLElement> = CallbackPromiseGeneric<{ el: T }>; +export type ElementCallbackValueFocus<T extends HTMLElement> = CallbackPromiseGeneric<{ el: T }>; +export type ElementCallbackMount<T extends HTMLElement> = CallbackPromiseGeneric<{ el: T }>; + +export type NavigationParamTuple<T extends string> = [T, string]; + +export type NavigationRouteParamId = `id`; +export type NavigationRouteParamField = `field`; +export type NavigationRouteParamRef = `ref`; +export type NavigationRouteParamLat = `lat`; +export type NavigationRouteParamLng = `lng`; +export type NavigationRouteParamNostrPublicKey = `key_nostr`; +export type NavigationRouteParamKey = NavigationRouteParamId | NavigationRouteParamField | NavigationRouteParamRef | NavigationRouteParamLat | NavigationRouteParamLng | NavigationRouteParamNostrPublicKey; +export type NavigationRouteParamTuple = NavigationParamTuple<NavigationRouteParamKey>; +export type NavigationPreviousParam<T extends string> = { route: T, label?: string; params?: NavigationRouteParamTuple[] } + +export type LocalCallbackColorMode = CallbackPromiseGeneric<ISelectOption<string>> +export type LocalCallbackGuiAlert = CallbackPromiseFull<string, boolean>; +export type LocalCallbackGuiConfirm = CallbackPromiseFull<string | { message: string; ok?: string; cancel?: string }, boolean>; +export type LocalCallbackGeocode = CallbackPromiseFull<GeolocationPoint, GeocoderReverseResult | undefined>; +export type LocalCallbackGeocodeCurrent = CallbackPromiseResult<IClientGeolocationPosition>; +export type LocalCallbackImgBin = CallbackPromiseFull<string, Uint8Array | undefined>; +export type LocalCallbackPhotosAdd = CallbackPromiseResult<string>; +export type LocalCallbackPhotosAddMultiple = CallbackPromiseResult<string[]>; +export type LocalCallbackPhotosUpload = CallbackPromiseFull<{ url: string, path: string }, MediaImageUploadResult | undefined>; diff --git a/apps-lib/src/lib/types/ui.ts b/apps-lib/src/lib/types/ui.ts @@ -0,0 +1,230 @@ +import type { ThemeLayer } from "@radroots/themes"; +import type { CallbackPromise, CallbackPromiseGeneric } from "@radroots/utils"; +import type { EntryStyle, IGlyph, IInput, IInputValue, ISelect, ITextArea } from "./components"; +import type { GlyphKey, LayerGlyphBasisKind, LoadingBlades, LoadingDimension } from "./lib"; + +export type IDisabled = { + disabled: boolean | never; +}; + +export type IDisabledOpt = Partial<IDisabled>; + +export type IBasisOpt<T extends object> = T | undefined; + +export type IBasis<T> = { + basis: T; +}; + +export type ICb = { + callback: CallbackPromise | never; +}; + +export type ICbOpt = Partial<ICb>; + +export type ICbG<T> = { + callback: CallbackPromiseGeneric<T> | never; +}; + +export type ICbMouseEventOpt = ICbGOpt< + MouseEvent & { + currentTarget: EventTarget & HTMLImageElement; + } +>; + +export type ICbGOpt<T> = Partial<ICbG<T>>; + +export type ICl = { + classes: string | never; +}; + +export type IClOpt = Partial<ICl>; + +export type IClWrap = { + classes_wr: string | never; +}; + +export type IClOptWrap = Partial<IClWrap>; + +export type IId = { + id: string | never; +}; + +export type IIdOpt = Partial<IId>; + +export type IGl = { + glyph: IGlyph | never; +}; + +export type IGlOpt = Partial<IGl>; + +export type IGlyphKey = { + glyph: GlyphKey +}; + +export type ILy = { + layer: ThemeLayer | never; +}; + +export type ILyOpt = Partial<ILy>; + +export type ILableFieldsSwap = { + toggle: boolean; + on: IClOpt & { + value: string; + }, + off: IClOpt & { + value: string; + }, +}; + +export type ILabelSwap = { + swap: ILableFieldsSwap; +} + +export type ILabelTupFields = { + left?: ILableFields[]; + right?: ILableFields[]; +}; + +export type ILabelTup = { + label: ILabelTupFields; +}; + +export type ILableFields = & { + classes_wrap?: string + classes?: string; + kind?: LayerGlyphBasisKind; + hide_truncate?: boolean; + hide_active?: boolean; +} & ( + ({ + value: string; + } | ILabelSwap) + | IGl + ); + +export type ILabel = { + label: ILableFields; +}; + +export type ILabelOpt = Partial<ILabel>; + +export type ILoadSymbol = IClOpt & { + color?: 'white'; + blades?: LoadingBlades; + dim?: LoadingDimension; +}; + +export type IIdG<T extends string> = { + id: T | never; +}; + +export type IIdGOpt<T extends string> = Partial<IIdG<T>>; + +export type IIdWrap = { + id_wrap: string | never; +}; + +export type IIdWrapOpt = Partial<IIdWrap>; + +export type ILabelValue = { + label: IClOpt & { + value: string; + }; +}; + +export type ILabelDisplay = IIdWrapOpt & IClOpt & ILabelValue & ILyOpt & { + style?: EntryStyle; +}; + + +export type ILoading = { + loading: boolean | never; +}; + +export type ILoadingOpt = Partial<ILoading>; + +export type IEntryWrap = IClOpt & IIdOpt & ILyOpt & { + style?: EntryStyle; + style_a?: true; + no_pad?: true; + fade?: { + in?: SvelteTransitionConfig; + out?: SvelteTransitionConfig; + }; +} + +export type IEntryLine = ILoadingOpt & { + wrap?: IEntryWrap; + el: IInputValue<string>; + notify_inline?: { + glyph: GlyphKey | IGlyph; + }; +}; + +export type IEntryLineIdb = ILoadingOpt & { + wrap?: IEntryWrap; + el: IInput<string>; + notify_inline?: { + glyph: GlyphKey | IGlyph; + }; +}; + +export type IEntryLineSelectIdb = ILoadingOpt & { + wrap?: IEntryWrap; + el_input: IInput<string>; + el_sel: ISelect; + /*notify_inline?: { + glyph: GlyphKey | IGlyph; + };*/ +}; + +export type IEntryMultiLine = { + wrap?: IEntryWrap; + el: ITextArea; + notify_inline?: { + glyph: GlyphKey | IGlyph; + }; +} + +export type IEnvelopeLower = { + visible: boolean; + close: CallbackPromise; + full_cover?: boolean; + label_close?: string | true; +}; + +export type IButtonRound = IClOpt & ILoadingOpt & { + label: string; + callback: CallbackPromise; +}; + +export type INavigationRoutePreventRouteNav = { + prevent_route?: { + callback: CallbackPromise; + }; +}; + +export type INavigationRoutePreventRoute = { + prevent_route: CallbackPromise; +}; + +export type IImage = IIdOpt & IClOpt & { + src?: string; + alt?: string; +}; + +export type IImageBlob = IIdOpt & IClOpt & { + data: Uint8Array | undefined; + alt?: string; +}; + +export type IImageSource = IIdOpt & IClOpt & { + src?: string; + alt?: string; +}; + +export type IImagePath = Omit<IImage, 'src'> & { + path?: string; +}; + diff --git a/apps-lib/src/lib/utils/app/carousel.ts b/apps-lib/src/lib/utils/app/carousel.ts @@ -0,0 +1,83 @@ + +import { carousel_active, carousel_num, casl_i, casl_imax } from "$lib/stores/carousel"; +import { exe_iter } from "@radroots/utils"; +import { get_store } from "../lib"; + +const CAROUSEL_DELAY_MS = 150; + +const get_slide_container = <T extends string>( + view: T, +): Element | undefined => { + const el = document.querySelector( + `[data-carousel-container="${view}"]`, + ); + return el ? el : undefined; +}; + +const get_slide_item = <T extends string>(view: T): Element | undefined => { + const el = document.querySelector(`[data-carousel-item="${view}"]`); + return el ? el : undefined; +}; + +const carousel_dec_handler = async <T extends string>( + view: T, +): Promise<void> => { + const $carousel_active = get_store(carousel_active); + if ($carousel_active) return; + carousel_active.set(true); + const slide_item = get_slide_item<T>(view); + const slide_container = get_slide_container<T>(view); + if (slide_container && slide_item) { + const slide_w = slide_item?.clientWidth || 0; + slide_container.scrollLeft -= slide_w; + const $casl_i = get_store(casl_i); + casl_i.set(Math.max($casl_i - 1, 0)); + } + carousel_active.set(false); +}; + +const carousel_inc_handler = async <T extends string>( + view: T, +): Promise<void> => { + const $carousel_active = get_store(carousel_active); + if ($carousel_active) return; + carousel_active.set(true); + const slide_item = get_slide_item<T>(view); + const slide_container = get_slide_container<T>(view); + if (slide_container && slide_item) { + const slide_w = slide_item?.clientWidth || 0; + slide_container.scrollLeft += slide_w; + const $casl_i = get_store(casl_i); + const $casl_imax = get_store(casl_imax); + casl_i.set( + Math.min($casl_i + 1, $casl_imax), + ); + } + carousel_active.set(false); +}; + +export const carousel_inc = async <T extends string>( + view: T, + duration: number = CAROUSEL_DELAY_MS +): Promise<void> => { + const $carousel_num = get_store(carousel_num); + carousel_num.set(1); + await exe_iter(async () => carousel_inc_handler(view), $carousel_num, duration); +}; + + +export const carousel_dec = async <T extends string>( + view: T, + duration: number = CAROUSEL_DELAY_MS +): Promise<void> => { + const $carousel_num = get_store(carousel_num); + carousel_num.set(1); + await exe_iter(async () => carousel_dec_handler(view), $carousel_num, duration); +}; + +export const carousel_init = async <T extends string>(view: T, num_max: number): Promise<void> => { + await carousel_dec(view); + casl_i.set(0); + casl_imax.set(num_max); + carousel_num.set(1); +}; diff --git a/apps-lib/src/lib/utils/browser.ts b/apps-lib/src/lib/utils/browser.ts @@ -0,0 +1,92 @@ +export type BrowserPlatformInfo = { + os: string; + browser: string; + version: string; +}; + +const remove_excess_mozilla_and_version = /^mozilla\/\d\.\d\W/; +const browser_pattern = /(\w+)\/(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)/g; +const engine_and_version_pattern = /^(ver|cri|gec)/; +const brand_list = ['chrome', 'opera', 'safari', 'edge', 'firefox']; + +const mobiles: Record<string, RegExp> = { + iphone: /iphone/, + ipad: /ipad|macintosh/, + android: /android/ +}; + +const desktops: Record<string, RegExp> = { + windows: /win/, + mac: /macintosh/, + linux: /linux/ +}; + +const parse_user_agent_string = (ua_string: string): BrowserPlatformInfo => { + const ua = ua_string.toLowerCase().replace(remove_excess_mozilla_and_version, ''); + + const mobile_os = Object.keys(mobiles).find( + (key) => mobiles[key].test(ua) && navigator.maxTouchPoints >= 1 + ); + const desktop_os = Object.keys(desktops).find((key) => desktops[key].test(ua)); + const os = mobile_os || desktop_os || ''; + + const browser_matches = ua.match(browser_pattern); + const version_regex = /version\/(\d+(\.\d+)*)/; + const safari_version_match = ua.match(version_regex); + const safari_version = Array.isArray(safari_version_match) ? safari_version_match[1] : null; + + const browser_offset = + browser_matches && browser_matches.length > 2 && !engine_and_version_pattern.test(browser_matches[1]) + ? 1 + : 0; + const browser_result = + browser_matches && browser_matches[browser_matches.length - 1 - browser_offset].split('/'); + const browser = browser_result ? browser_result[0] : ''; + const version = safari_version || (browser_result ? browser_result[1] : ''); + + return { os, browser, version }; +}; + +export const browser_platform = (): BrowserPlatformInfo | undefined => { + if (typeof navigator !== 'undefined') { + if ('userAgentData' in navigator && navigator.userAgentData) { + const ua_data = navigator.userAgentData as { + platform: string; + brands: { brand: string; version: string }[]; + }; + const os = ua_data.platform.toLowerCase(); + let browser = ''; + let version = ''; + + if (Array.isArray(ua_data.brands)) { + for (const { brand, version: brand_version } of ua_data.brands) { + const lower_brand = brand.toLowerCase(); + if (brand_list.some((b) => lower_brand.includes(b))) { + browser = lower_brand; + version = brand_version; + break; + } + } + } + + if (!browser && navigator.userAgent) { + return parse_user_agent_string(navigator.userAgent); + } + return { os, browser, version }; + } + + if (navigator.userAgent) { + return parse_user_agent_string(navigator.userAgent); + } + + const nav_platform = navigator.platform; + if (!nav_platform) return undefined; + return { + os: nav_platform, + browser: '', + version: '' + }; + } + + return undefined; +}; diff --git a/apps-lib/src/lib/utils/geo.ts b/apps-lib/src/lib/utils/geo.ts @@ -0,0 +1,8 @@ +import type { GeolocationPoint } from "@radroots/utils"; + +export const geop_is_valid = (point?: GeolocationPoint): boolean => { + if (!point) return false; + return !(point.lat === 0 && point.lng === 0); +}; + +export const geop_init = (): GeolocationPoint => ({ lat: 0, lng: 0 }); diff --git a/apps-lib/src/lib/utils/idb/kv.ts b/apps-lib/src/lib/utils/idb/kv.ts @@ -0,0 +1,54 @@ +import { browser } from "$app/environment"; +import { fmt_id } from "../lib"; + +//@ts-ignore +const idb_name = import.meta.env.VITE_PUBLIC_IDB_NAME; +if (!idb_name) throw new Error('Error: VITE_PUBLIC_IDB_NAME is required'); + +export let idb: Keyva; +if (browser) idb = new Keyva({ name: idb_name }); + +export const idb_init = async (): Promise<void> => { + if (!browser) return; + const range = Keyva.prefix(`*`); + const idb_list = await idb.each({ range }, `keys`); + await Promise.all(idb_list.map((i) => idb.delete(i))); +}; + +export const idb_init_page = async (): Promise<void> => { + if (!browser) return; + const idb_pref = fmt_id(); + const range = Keyva.prefix(idb_pref); + const idb_list = await idb.each({ range }, `keys`); + await Promise.all(idb_list.map((i) => idb.delete(i))); +}; + +export const idb_sync = async (list: [string, string][]): Promise<void> => { + if (!browser) return; + for (const [key, val] of list) await idb.set(key, val); +}; + +export class IdbLib<T extends string> { + private _idb: Keyva; + + constructor(kv: Keyva) { + this._idb = kv; + } + public init = async () => { + await idb_init_page(); + } + + public save = async (key: T, value: string) => { + await this._idb.set(fmt_id(key), value); + } + + public read = async (key: T): Promise<string | undefined> => { + const result = await this._idb.get<string>(fmt_id(key)); + if (result) return result; + return undefined; + } + + public del = async (key: T) => { + await this._idb.delete(fmt_id(key)); + } +} +\ No newline at end of file diff --git a/apps-lib/src/lib/utils/idb/lib.ts b/apps-lib/src/lib/utils/idb/lib.ts @@ -1,86 +0,0 @@ -import { browser } from '$app/environment'; -import { fmt_id } from '$lib'; - -const IDB_NAME = import.meta.env.VITE_PUBLIC_IDB_NAME; -if (!IDB_NAME) throw new Error('VITE_PUBLIC_IDB_NAME is required'); - -let _kv: Keyva | null = null; -export function get_idb(): Keyva { - if (!browser) throw new Error('IndexedDB not available on server'); - if (!_kv) _kv = new Keyva({ name: IDB_NAME }); - return _kv; -} - -export class IdbLib< - CKey extends string, - CMap extends Record<CKey, any>, - SKey extends string, - SMap extends Record<SKey, any> -> { - private get kv(): Keyva { - return get_idb(); - } - - public async save_global(key: CKey, value: CMap[CKey]): Promise<void> { - await this.kv.set(key, value); - } - - public async read_global<K extends CKey>(key: K): Promise<CMap[K] | undefined> { - const result = await this.kv.get<CMap[K]>(key); - return result ? result : undefined; - } - - public async del_global(key: CKey): Promise<void> { - await this.kv.delete(key); - } - - public async list_global(): Promise<Array<[CKey, CMap[CKey]]>> { - const entries = await this.kv.each<CMap[CKey]>(); - return entries.map(([rk, v]) => [rk as CKey, v]); - } - - private keyof(key: SKey): string { - return fmt_id(key); - } - - public async save(key: SKey, value: SMap[SKey]): Promise<void> { - await this.kv.set(this.keyof(key), value); - } - - public async read(key: SKey): Promise<SMap[SKey] | undefined> { - const result = await this.kv.get<SMap[SKey]>(this.keyof(key)); - return result ? result : undefined; - } - - public async del(key: SKey): Promise<void> { - await this.kv.delete(this.keyof(key)); - } - - public async list(): Promise<Array<[SKey, SMap[SKey]]>> { - const prefix = fmt_id(); - const range = Keyva.prefix(prefix); - const entries = await this.kv.each<SMap[SKey]>({ range }); - return entries.map(([rk, v]) => { - const str = String(rk); - const suffix = str.startsWith(prefix) ? str.slice(prefix.length) : str; - return [suffix as SKey, v]; - }); - } - - public async clear_all(): Promise<void> { - const range = Keyva.prefix('*'); - const keys = await this.kv.each({ range }, 'keys'); - if (keys.length) await Promise.all(keys.map((k) => this.kv.delete(k))); - } - - public async clear_scope(): Promise<void> { - const prefix = fmt_id(); - const range = Keyva.prefix(prefix); - const keys = await this.kv.each({ range }, 'keys'); - if (keys.length) await Promise.all(keys.map((k) => this.kv.delete(k))); - } - - public async sync_batch(entries: Array<[string, any]>): Promise<void> { - if (entries.length) await this.kv.set(entries); - } -} diff --git a/apps-lib/src/lib/utils/lib.ts b/apps-lib/src/lib/utils/lib.ts @@ -1,9 +1,28 @@ import { browser } from '$app/environment'; -import type { ThemeMode } from '$lib/types/lib'; +import { goto } from '$app/navigation'; +import { win_h, win_w } from '$lib/stores/app'; +import type { CallbackRoute, NavigationParamTuple, NavigationRouteParamKey, NavigationRouteParamTuple, } from '$lib/types/lib'; +import type { ThemeLayer, ThemeMode } from '@radroots/themes'; +import type { FilePath } from '@radroots/utils'; +import { getContext, setContext } from "svelte"; import { get } from "svelte/store"; +export const symbols = { + bullet: '•', + dash: `—`, + up: `↑`, + down: `↓`, + percent: `%` +}; + export const get_store = get; +export const get_context = <M extends Record<string, any>, K extends keyof M>(key: K): M[K] => + getContext(key as string) as M[K]; + +export const set_context = <M extends Record<string, any>, K extends keyof M>(key: K, value: M[K]): void => + setContext(key as string, value); + export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export const trim_slashes = (path: string): string => @@ -27,6 +46,19 @@ export const fmt_id = (raw_id?: string): string => { return `*${prefix}${suffix}`; }; +export const view_effect = <T extends string>(view: T): void => { + if (!browser) return; + for (const el of document.querySelectorAll(`[data-view]`)) { + if (el.getAttribute(`data-view`) !== view) el.classList.add(`hidden`) + else el.classList.remove(`hidden`) + } +}; + +export const el_id = (id: string): HTMLElement | undefined => { + const el = document.getElementById(id); + return el ? el : undefined; +}; + export const build_storage_key = ( raw_id: string, base_prefix: string @@ -41,4 +73,118 @@ export const get_system_theme = (): ThemeMode => { export const theme_set = (theme_key: string, color_mode: ThemeMode): void => { document.documentElement.setAttribute("data-theme", `${theme_key}_${color_mode}`); }; -export const fmt_cl = (classes?: string): string => `${classes || ``}`; -\ No newline at end of file +export const fmt_cl = (classes?: string): string => `${classes || ``}`; + +export const handle_err = async (e: unknown, fcall: string): Promise<void> => { + try { + console.log(`[handle_err] `, e, fcall) + /*return void await catch_err(e, fcall, async (opts) => { + console.log(`handle_err e `, e) + console.log(JSON.stringify(opts, null, 4), `handle_err opts`) + });*/ + } catch (e) { + console.log(`(handle_err) `, e) + } +}; + +export const window_set = (): void => { + if (!browser) return; + win_h.set(window.innerHeight); + win_w.set(window.innerWidth); +}; + +export const parse_layer = (layer?: number, layer_default?: ThemeLayer): ThemeLayer => { + switch (layer) { + case 0: + case 1: + case 2: + return layer; + default: + return layer_default ? layer_default : 0; + }; +}; + +export const value_constrain = (regex_charset: RegExp, value: string): string => { + return value + .split(``) + .filter((char) => regex_charset.test(char)) + .join(``); +}; + + +export const encode_query_params = <T extends string>(params_list: NavigationParamTuple<T>[] = []): string => { + let query = ""; + for (const [k, v] of params_list) { + if (k && v) { + if (query) query += `&`; + query += `${k.trim()}=${encodeURIComponent(v.trim())}`; + } + } + return query ? `?${query}` : ``; +}; + +export const encode_route = <TRoute extends string, TParam extends string>(route: TRoute, params_list?: NavigationParamTuple<TParam>[]): string => { + const query = encode_query_params(params_list); + if (!query) return route; + return `${route === `/` ? `/` : route.replace(/\/+$/, ``)}${query}`; +}; + +export const debounce = <T extends (...args: any[]) => void>( + fn: T, + delay: number +): T => { + let timeout: ReturnType<typeof setTimeout>; + return ((...args: any[]) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(...args), delay); + }) as T; +}; + +export const create_router = <T extends string>() => { + const router = async (nav_route: T, params: NavigationRouteParamTuple[] = []): Promise<void> => { + try { + if (params.length) await goto(encode_route<T, NavigationRouteParamKey>(nav_route, params)); + else await goto(nav_route); + } catch (e) { + handle_err(e, `route`); + }; + }; + return router; +}; + +export const get_locale = (locales: string[]): string => { + const { language: navigator_locale } = navigator; + let locale = `en`; + if (locales.some(i => i === navigator_locale.toLowerCase())) locale = navigator.language; + else if (locales.some(i => i === navigator_locale.slice(0, 2).toLowerCase())) locale = navigator_locale.slice(0, 2); + return locale.toLowerCase(); +}; + +export const callback_route = async <T extends string>(callback_route: CallbackRoute<T>): Promise<void> => { + if (`route` in callback_route) { + if (typeof callback_route.route === `string`) return void await goto(callback_route.route); + else return void await goto( + encode_route<string, NavigationRouteParamKey>( + callback_route.route[0], + callback_route.route[1], + ), + ); + } + return void await callback_route(); +}; + +export const to_arr_buf = (u8: Uint8Array): ArrayBuffer => { + return u8.slice().buffer; +}; + +export const parse_file_path = (file_path: string): FilePath | undefined => { + const file_path_spl = file_path.split(`/`); + const file_path_file = file_path_spl[file_path_spl.length - 1] || ``; + const [file_name, mime_type] = file_path_file.split(`.`); + if (!file_name || !mime_type) return undefined; + return { + file_path, + file_name, + mime_type + }; +}; +\ No newline at end of file diff --git a/apps-lib/vite.config.ts b/apps-lib/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] });