web_lib

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

commit ef92d54a393c7c1dc81906de910bc7e182f4d9f0
parent c5b4b5c5fb452d2220329c2a261aa6396478e9b1
Author: triesap <triesap@radroots.dev>
Date:   Fri, 26 Dec 2025 17:50:38 +0000

components: add ViewStack and ViewPane primitives

- Export ViewStack and ViewPane from components index
- Define typed view context contract and context key
- Implement ViewPane state derivation with transitions and pointer-events gating
- Implement ViewStack context provider with reactive basis sync and layout styling

Diffstat:
Mapps-lib/src/lib/components/index.ts | 2++
Aapps-lib/src/lib/components/view-context.ts | 19+++++++++++++++++++
Aapps-lib/src/lib/components/view-pane.svelte | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps-lib/src/lib/components/view-stack.svelte | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 305 insertions(+), 0 deletions(-)

diff --git a/apps-lib/src/lib/components/index.ts b/apps-lib/src/lib/components/index.ts @@ -5,3 +5,5 @@ export { default as Glyphi } from "./glyphi.svelte"; export { default as ImageBlob } from "./image-blob.svelte"; export { default as ImageSrc } from "./image-src.svelte"; export { default as Input } from "./input.svelte"; +export { default as ViewPane } from "./view-pane.svelte"; +export { default as ViewStack } from "./view-stack.svelte"; diff --git a/apps-lib/src/lib/components/view-context.ts b/apps-lib/src/lib/components/view-context.ts @@ -0,0 +1,19 @@ +export type ViewMode = "stack" | "flow"; +export type ViewPointerEvents = "none" | "auto"; + +export type ViewContext<T extends string> = { + active_view: T; + mode: ViewMode; + fade: boolean; + transition_ms: number; + opacity_inactive: number; + scale_inactive: number; + offset_x: string; + offset_y: string; + blur_inactive_px: number; + pointer_events_inactive: ViewPointerEvents; + z_index_active: number; + z_index_inactive: number; +}; + +export const VIEW_CONTEXT_KEY = "radroots:view"; diff --git a/apps-lib/src/lib/components/view-pane.svelte b/apps-lib/src/lib/components/view-pane.svelte @@ -0,0 +1,169 @@ +<script lang="ts"> + import type { Snippet } from "svelte"; + import { get_context } from "$lib/utils/app"; + import { + VIEW_CONTEXT_KEY, + type ViewContext, + type ViewMode, + type ViewPointerEvents, + } from "./view-context"; + + const DEFAULT_TRANSITION_MS = 260; + const DEFAULT_OPACITY_INACTIVE = 0; + const DEFAULT_SCALE_INACTIVE = 0.98; + const DEFAULT_OFFSET_X = "0px"; + const DEFAULT_OFFSET_Y = "12px"; + const DEFAULT_BLUR_INACTIVE_PX = 0; + const DEFAULT_POINTER_EVENTS_INACTIVE: ViewPointerEvents = "none"; + const DEFAULT_Z_INDEX_ACTIVE = 2; + const DEFAULT_Z_INDEX_INACTIVE = 1; + const DEFAULT_DISPLAY = "flex"; + const DEFAULT_DIRECTION = "column"; + const DEFAULT_ALIGN = "stretch"; + const DEFAULT_JUSTIFY = "flex-start"; + const DEFAULT_WIDTH = "100%"; + const DEFAULT_HEIGHT = "100%"; + const DEFAULT_MIN_HEIGHT = "0px"; + const DEFAULT_PADDING = "0px"; + const DEFAULT_GAP = "0px"; + const DEFAULT_OVERFLOW = "hidden"; + const DEFAULT_BACKGROUND = "transparent"; + const EASE_OUT = "cubic-bezier(0.16, 1, 0.3, 1)"; + + type ViewPaneBasis<T extends string> = { + view: T; + active?: boolean; + active_view?: T; + mode?: ViewMode; + fade?: boolean; + transition_ms?: number; + opacity_inactive?: number; + scale_inactive?: number; + offset_x?: string; + offset_y?: string; + blur_inactive_px?: number; + pointer_events_inactive?: ViewPointerEvents; + z_index_active?: number; + z_index_inactive?: number; + display?: string; + direction?: string; + align?: string; + justify?: string; + width?: string; + height?: string; + min_height?: string; + padding?: string; + gap?: string; + overflow?: string; + background?: string; + style?: string; + style_active?: string; + style_inactive?: string; + role?: string; + tabindex?: number; + aria_label?: string; + }; + + let { + basis, + children, + }: { + basis: ViewPaneBasis<string>; + children: Snippet; + } = $props(); + + const view_context = + get_context<ViewContext<string> | undefined>(VIEW_CONTEXT_KEY); + + const active_view = $derived(basis.active_view ?? view_context?.active_view); + const is_active = $derived( + basis.active !== undefined + ? basis.active + : active_view + ? basis.view === active_view + : true, + ); + const mode = $derived(basis.mode ?? view_context?.mode ?? "stack"); + const fade = $derived(basis.fade ?? view_context?.fade ?? true); + const transition_ms = $derived( + basis.transition_ms ?? view_context?.transition_ms ?? DEFAULT_TRANSITION_MS, + ); + const opacity_inactive = $derived( + basis.opacity_inactive ?? + view_context?.opacity_inactive ?? + DEFAULT_OPACITY_INACTIVE, + ); + const scale_inactive = $derived( + basis.scale_inactive ?? + view_context?.scale_inactive ?? + DEFAULT_SCALE_INACTIVE, + ); + const offset_x = $derived( + basis.offset_x ?? view_context?.offset_x ?? DEFAULT_OFFSET_X, + ); + const offset_y = $derived( + basis.offset_y ?? view_context?.offset_y ?? DEFAULT_OFFSET_Y, + ); + const blur_inactive_px = $derived( + basis.blur_inactive_px ?? + view_context?.blur_inactive_px ?? + DEFAULT_BLUR_INACTIVE_PX, + ); + const pointer_events_inactive = $derived( + basis.pointer_events_inactive ?? + view_context?.pointer_events_inactive ?? + DEFAULT_POINTER_EVENTS_INACTIVE, + ); + const z_index_active = $derived( + basis.z_index_active ?? + view_context?.z_index_active ?? + DEFAULT_Z_INDEX_ACTIVE, + ); + const z_index_inactive = $derived( + basis.z_index_inactive ?? + view_context?.z_index_inactive ?? + DEFAULT_Z_INDEX_INACTIVE, + ); + const display = $derived(basis.display ?? DEFAULT_DISPLAY); + const direction = $derived(basis.direction ?? DEFAULT_DIRECTION); + const align = $derived(basis.align ?? DEFAULT_ALIGN); + const justify = $derived(basis.justify ?? DEFAULT_JUSTIFY); + const width = $derived(basis.width ?? DEFAULT_WIDTH); + const height = $derived(basis.height ?? DEFAULT_HEIGHT); + const min_height = $derived(basis.min_height ?? DEFAULT_MIN_HEIGHT); + const padding = $derived(basis.padding ?? DEFAULT_PADDING); + const gap = $derived(basis.gap ?? DEFAULT_GAP); + const overflow = $derived(basis.overflow ?? DEFAULT_OVERFLOW); + const background = $derived(basis.background ?? DEFAULT_BACKGROUND); + + const opacity = $derived(is_active ? 1 : opacity_inactive); + const transform = $derived( + `translate3d(${is_active ? "0px" : offset_x}, ${is_active ? "0px" : offset_y}, 0) scale(${is_active ? 1 : scale_inactive})`, + ); + const blur_val = $derived(is_active ? 0 : blur_inactive_px); + const filter = $derived(blur_val ? `blur(${blur_val}px)` : "none"); + const pointer_events = $derived(is_active ? "auto" : pointer_events_inactive); + const z_index = $derived(is_active ? z_index_active : z_index_inactive); + const transition = $derived( + fade + ? `opacity ${transition_ms}ms ${EASE_OUT}, transform ${transition_ms}ms ${EASE_OUT}, filter ${transition_ms}ms ${EASE_OUT}` + : `transform ${transition_ms}ms ${EASE_OUT}, filter ${transition_ms}ms ${EASE_OUT}`, + ); + const position = $derived(mode === "stack" ? "absolute" : "relative"); + const inset = $derived(mode === "stack" ? "0" : "auto"); + + const style = $derived( + `position: ${position}; inset: ${inset}; width: ${width}; height: ${height}; min-height: ${min_height}; display: ${display}; flex-direction: ${direction}; align-items: ${align}; justify-content: ${justify}; gap: ${gap}; padding: ${padding}; overflow: ${overflow}; background: ${background}; opacity: ${opacity}; transform: ${transform}; filter: ${filter}; transition: ${transition}; pointer-events: ${pointer_events}; z-index: ${z_index}; box-sizing: border-box; will-change: transform, opacity, filter; backface-visibility: hidden; transform-style: preserve-3d; ${basis.style ?? ""} ${is_active ? basis.style_active ?? "" : basis.style_inactive ?? ""}`, + ); +</script> + +<div + data-view={basis.view} + style={style} + role={basis.role ?? undefined} + tabindex={basis.tabindex ?? undefined} + aria-label={basis.aria_label ?? undefined} + aria-hidden={!is_active} +> + {@render children()} +</div> diff --git a/apps-lib/src/lib/components/view-stack.svelte b/apps-lib/src/lib/components/view-stack.svelte @@ -0,0 +1,115 @@ +<script lang="ts"> + import type { Snippet } from "svelte"; + import { set_context } from "$lib/utils/app"; + import { + VIEW_CONTEXT_KEY, + type ViewContext, + type ViewMode, + type ViewPointerEvents, + } from "./view-context"; + + const DEFAULT_TRANSITION_MS = 260; + const DEFAULT_OPACITY_INACTIVE = 0; + const DEFAULT_SCALE_INACTIVE = 0.98; + const DEFAULT_OFFSET_X = "0px"; + const DEFAULT_OFFSET_Y = "12px"; + const DEFAULT_BLUR_INACTIVE_PX = 0; + const DEFAULT_POINTER_EVENTS_INACTIVE: ViewPointerEvents = "none"; + const DEFAULT_Z_INDEX_ACTIVE = 2; + const DEFAULT_Z_INDEX_INACTIVE = 1; + + type ViewStackBasis<T extends string> = { + active_view: T; + mode?: ViewMode; + width?: string; + height?: string; + min_height?: string; + padding?: string; + gap?: string; + direction?: string; + align?: string; + justify?: string; + overflow?: string; + background?: string; + style?: string; + fade?: boolean; + transition_ms?: number; + opacity_inactive?: number; + scale_inactive?: number; + offset_x?: string; + offset_y?: string; + blur_inactive_px?: number; + pointer_events_inactive?: ViewPointerEvents; + z_index_active?: number; + z_index_inactive?: number; + }; + + let { + basis, + children, + }: { + basis: ViewStackBasis<string>; + children: Snippet; + } = $props(); + + const view_context = $state<ViewContext<string>>({ + active_view: basis.active_view, + mode: basis.mode ?? "stack", + fade: basis.fade ?? true, + transition_ms: basis.transition_ms ?? DEFAULT_TRANSITION_MS, + opacity_inactive: basis.opacity_inactive ?? DEFAULT_OPACITY_INACTIVE, + scale_inactive: basis.scale_inactive ?? DEFAULT_SCALE_INACTIVE, + offset_x: basis.offset_x ?? DEFAULT_OFFSET_X, + offset_y: basis.offset_y ?? DEFAULT_OFFSET_Y, + blur_inactive_px: basis.blur_inactive_px ?? DEFAULT_BLUR_INACTIVE_PX, + pointer_events_inactive: + basis.pointer_events_inactive ?? DEFAULT_POINTER_EVENTS_INACTIVE, + z_index_active: basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE, + z_index_inactive: basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE, + }); + + set_context(VIEW_CONTEXT_KEY, view_context); + + $effect(() => { + view_context.active_view = basis.active_view; + view_context.mode = basis.mode ?? "stack"; + view_context.fade = basis.fade ?? true; + view_context.transition_ms = + basis.transition_ms ?? DEFAULT_TRANSITION_MS; + view_context.opacity_inactive = + basis.opacity_inactive ?? DEFAULT_OPACITY_INACTIVE; + view_context.scale_inactive = + basis.scale_inactive ?? DEFAULT_SCALE_INACTIVE; + view_context.offset_x = basis.offset_x ?? DEFAULT_OFFSET_X; + view_context.offset_y = basis.offset_y ?? DEFAULT_OFFSET_Y; + view_context.blur_inactive_px = + basis.blur_inactive_px ?? DEFAULT_BLUR_INACTIVE_PX; + view_context.pointer_events_inactive = + basis.pointer_events_inactive ?? DEFAULT_POINTER_EVENTS_INACTIVE; + view_context.z_index_active = + basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE; + view_context.z_index_inactive = + basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE; + }); + + const mode = $derived(basis.mode ?? "stack"); + const width = $derived(basis.width ?? "100%"); + const height = $derived(basis.height ?? "100%"); + const min_height = $derived(basis.min_height ?? "0px"); + const padding = $derived(basis.padding ?? "0px"); + const gap = $derived(basis.gap ?? "0px"); + const direction = $derived(basis.direction ?? "column"); + const align = $derived(basis.align ?? "stretch"); + const justify = $derived(basis.justify ?? "flex-start"); + const overflow = $derived(basis.overflow ?? "hidden"); + const background = $derived(basis.background ?? "transparent"); + const display = $derived(mode === "flow" ? "flex" : "block"); + + const style = $derived( + `position: relative; width: ${width}; height: ${height}; min-height: ${min_height}; padding: ${padding}; display: ${display}; flex-direction: ${direction}; align-items: ${align}; justify-content: ${justify}; gap: ${gap}; overflow: ${overflow}; background: ${background}; isolation: isolate; contain: layout paint style; ${basis.style ?? ""}`, + ); +</script> + +<div style={style}> + {@render children()} +</div>