feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
501
web/node_modules/svelte-sonner/dist/Toast.svelte
generated
vendored
Normal file
501
web/node_modules/svelte-sonner/dist/Toast.svelte
generated
vendored
Normal file
@@ -0,0 +1,501 @@
|
||||
<script lang="ts" module>"use strict";
|
||||
// Default lifetime of a toasts (in ms)
|
||||
const TOAST_LIFETIME = 4000;
|
||||
// Default gap between toasts
|
||||
const GAP = 14;
|
||||
// Threshold to dismiss a toast
|
||||
const SWIPE_THRESHOLD = 45;
|
||||
// Equal to exit animation duration
|
||||
const TIME_BEFORE_UNMOUNT = 200;
|
||||
const SCALE_MULTIPLIER = 0.05;
|
||||
const DEFAULT_TOAST_CLASSES = {
|
||||
toast: '',
|
||||
title: '',
|
||||
description: '',
|
||||
loader: '',
|
||||
closeButton: '',
|
||||
cancelButton: '',
|
||||
actionButton: '',
|
||||
action: '',
|
||||
warning: '',
|
||||
error: '',
|
||||
success: '',
|
||||
default: '',
|
||||
info: '',
|
||||
loading: ''
|
||||
};
|
||||
function getDefaultSwipeDirections(position) {
|
||||
const [y, x] = position.split('-');
|
||||
const directions = [];
|
||||
if (y) {
|
||||
directions.push(y);
|
||||
}
|
||||
if (x) {
|
||||
directions.push(x);
|
||||
}
|
||||
return directions;
|
||||
}
|
||||
function getDampening(delta) {
|
||||
const factor = Math.abs(delta) / 20;
|
||||
return 1 / (1.5 + factor);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">import { onMount, untrack } from 'svelte';
|
||||
import { isAction } from './types.js';
|
||||
import { toastState } from './toast-state.svelte.js';
|
||||
import { cn } from './internal/helpers.js';
|
||||
import { useDocumentHidden } from './internal/use-document-hidden.svelte.js';
|
||||
import Loader from './Loader.svelte';
|
||||
let { toast, index, expanded, invert: invertFromToaster, position, visibleToasts, expandByDefault, closeButton: closeButtonFromToaster, interacting, cancelButtonStyle = '', actionButtonStyle = '', duration: durationFromToaster, descriptionClass = '', classes: classesProp, unstyled = false, loadingIcon, successIcon, errorIcon, warningIcon, closeIcon, infoIcon, defaultRichColors = false, swipeDirections: swipeDirectionsProp, closeButtonAriaLabel, pauseWhenPageIsHidden, ...restProps } = $props();
|
||||
const defaultClasses = { ...DEFAULT_TOAST_CLASSES };
|
||||
let mounted = $state(false);
|
||||
let removed = $state(false);
|
||||
let swiping = $state(false);
|
||||
let swipeOut = $state(false);
|
||||
let isSwiped = $state(false);
|
||||
let offsetBeforeRemove = $state(0);
|
||||
let initialHeight = $state(0);
|
||||
let remainingTime = toast.duration || durationFromToaster || TOAST_LIFETIME;
|
||||
let dragStartTime = $state(null);
|
||||
let toastRef = $state();
|
||||
let swipeDirection = $state(null);
|
||||
let swipeOutDirection = $state(null);
|
||||
const isFront = $derived(index === 0);
|
||||
const isVisible = $derived(index + 1 <= visibleToasts);
|
||||
const toastType = $derived(toast.type);
|
||||
const dismissible = $derived(toast.dismissible !== undefined
|
||||
? toast.dismissible !== false
|
||||
: toast.dismissable !== false);
|
||||
const toastClass = $derived(toast.class || '');
|
||||
const toastDescriptionClass = $derived(toast.descriptionClass || '');
|
||||
// height index is used to calculate the offset as it gets updated before the toast array, which means we can calculate the new layout faster.
|
||||
const heightIndex = $derived(toastState.heights.findIndex((height) => height.toastId === toast.id) ||
|
||||
0);
|
||||
const closeButton = $derived(toast.closeButton ?? closeButtonFromToaster);
|
||||
const duration = $derived(toast.duration ?? durationFromToaster ?? TOAST_LIFETIME);
|
||||
let pointerStart = null;
|
||||
const coords = $derived(position.split('-'));
|
||||
const toastsHeightBefore = $derived(toastState.heights.reduce((prev, curr, reducerIndex) => {
|
||||
if (reducerIndex >= heightIndex)
|
||||
return prev;
|
||||
return prev + curr.height;
|
||||
}, 0));
|
||||
const isDocumentHidden = useDocumentHidden();
|
||||
const invert = $derived(toast.invert || invertFromToaster);
|
||||
const disabled = $derived(toastType === 'loading');
|
||||
const classes = $derived({ ...defaultClasses, ...classesProp });
|
||||
const toastTitle = $derived(toast.title);
|
||||
const toastDescription = $derived(toast.description);
|
||||
let closeTimerStartTime = $state(0);
|
||||
let lastCloseTimerStartTime = $state(0);
|
||||
const offset = $derived(Math.round(heightIndex * GAP + toastsHeightBefore));
|
||||
$effect(() => {
|
||||
toastTitle;
|
||||
toastDescription;
|
||||
let scale;
|
||||
if (expanded || expandByDefault) {
|
||||
scale = 1;
|
||||
}
|
||||
else {
|
||||
scale = 1 - index * SCALE_MULTIPLIER;
|
||||
}
|
||||
const toastEl = untrack(() => toastRef);
|
||||
if (toastEl === undefined)
|
||||
return;
|
||||
toastEl.style.setProperty('height', 'auto');
|
||||
const offsetHeight = toastEl.offsetHeight;
|
||||
const rectHeight = toastEl.getBoundingClientRect().height;
|
||||
const scaledRectHeight = Math.round((rectHeight / scale + Number.EPSILON) & 100) / 100;
|
||||
toastEl.style.removeProperty('height');
|
||||
let finalHeight;
|
||||
if (Math.abs(scaledRectHeight - offsetHeight) < 1) {
|
||||
// use scaledRectHeight as it's more precise
|
||||
finalHeight = scaledRectHeight;
|
||||
}
|
||||
else {
|
||||
// toast was transitioning its scale, so scaledRectHeight isn't accurate
|
||||
finalHeight = offsetHeight;
|
||||
}
|
||||
initialHeight = finalHeight;
|
||||
toastState.setHeight({ toastId: toast.id, height: finalHeight });
|
||||
});
|
||||
function deleteToast() {
|
||||
removed = true;
|
||||
// save the offset for the exit swipe animation
|
||||
offsetBeforeRemove = offset;
|
||||
toastState.removeHeight(toast.id);
|
||||
setTimeout(() => {
|
||||
toastState.remove(toast.id);
|
||||
}, TIME_BEFORE_UNMOUNT);
|
||||
}
|
||||
let timeoutId;
|
||||
const isPromiseLoadingOrInfiniteDuration = $derived((toast.promise && toastType === 'loading') ||
|
||||
toast.duration === Number.POSITIVE_INFINITY);
|
||||
function startTimer() {
|
||||
closeTimerStartTime = new Date().getTime();
|
||||
// let the toast know it has started
|
||||
timeoutId = setTimeout(() => {
|
||||
toast.onAutoClose?.(toast);
|
||||
deleteToast();
|
||||
}, remainingTime);
|
||||
}
|
||||
function pauseTimer() {
|
||||
if (lastCloseTimerStartTime < closeTimerStartTime) {
|
||||
// get the elapsed time since the timer started
|
||||
const elapsedTime = new Date().getTime() - closeTimerStartTime;
|
||||
remainingTime = remainingTime - elapsedTime;
|
||||
}
|
||||
lastCloseTimerStartTime = new Date().getTime();
|
||||
}
|
||||
$effect(() => {
|
||||
if (toast.updated) {
|
||||
// if the toast has been updated after the initial render,
|
||||
// we want to reset the timer and set the remaining time to the
|
||||
// new duration
|
||||
clearTimeout(timeoutId);
|
||||
remainingTime = duration;
|
||||
if (!isPromiseLoadingOrInfiniteDuration) {
|
||||
startTimer();
|
||||
}
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (!isPromiseLoadingOrInfiniteDuration) {
|
||||
if (expanded || interacting || (pauseWhenPageIsHidden && isDocumentHidden.current)) {
|
||||
pauseTimer();
|
||||
}
|
||||
else {
|
||||
startTimer();
|
||||
}
|
||||
}
|
||||
return () => clearTimeout(timeoutId);
|
||||
});
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
const height = toastRef?.getBoundingClientRect().height;
|
||||
initialHeight = height;
|
||||
toastState.setHeight({ toastId: toast.id, height });
|
||||
return () => {
|
||||
toastState.removeHeight(toast.id);
|
||||
};
|
||||
});
|
||||
$effect(() => {
|
||||
if (toast.delete) {
|
||||
untrack(() => {
|
||||
deleteToast();
|
||||
toast.onDismiss?.(toast);
|
||||
});
|
||||
}
|
||||
});
|
||||
const handlePointerDown = (event) => {
|
||||
if (disabled)
|
||||
return;
|
||||
offsetBeforeRemove = offset;
|
||||
const target = event.target;
|
||||
// ensure we maintain correct pointer capture even when going outside of the toast (e.g. when swiping)
|
||||
target.setPointerCapture(event.pointerId);
|
||||
if (target.tagName === 'BUTTON')
|
||||
return;
|
||||
swiping = true;
|
||||
pointerStart = { x: event.clientX, y: event.clientY };
|
||||
};
|
||||
const handlePointerUp = () => {
|
||||
if (swipeOut || !dismissible)
|
||||
return;
|
||||
pointerStart = null;
|
||||
const swipeAmountX = Number(toastRef?.style
|
||||
.getPropertyValue('--swipe-amount-x')
|
||||
.replace('px', '') || 0);
|
||||
const swipeAmountY = Number(toastRef?.style
|
||||
.getPropertyValue('--swipe-amount-y')
|
||||
.replace('px', '') || 0);
|
||||
const timeTaken = new Date().getTime() - (dragStartTime?.getTime() ?? 0);
|
||||
const swipeAmount = swipeDirection === 'x' ? swipeAmountX : swipeAmountY;
|
||||
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||
// remove only if threshold is met
|
||||
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
|
||||
offsetBeforeRemove = offset;
|
||||
toast.onDismiss?.(toast);
|
||||
if (swipeDirection === 'x') {
|
||||
swipeOutDirection = swipeAmountX > 0 ? 'right' : 'left';
|
||||
}
|
||||
else {
|
||||
swipeOutDirection = swipeAmountY > 0 ? 'down' : 'up';
|
||||
}
|
||||
deleteToast();
|
||||
swipeOut = true;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
toastRef?.style.setProperty('--swipe-amount-x', '0px');
|
||||
toastRef?.style.setProperty('--swipe-amount-y', '0px');
|
||||
}
|
||||
isSwiped = false;
|
||||
swiping = false;
|
||||
swipeDirection = null;
|
||||
};
|
||||
const handlePointerMove = (event) => {
|
||||
if (!pointerStart || !dismissible)
|
||||
return;
|
||||
const isHighlighted = (window.getSelection()?.toString().length ?? -1) > 0;
|
||||
if (isHighlighted)
|
||||
return;
|
||||
const yDelta = event.clientY - pointerStart.y;
|
||||
const xDelta = event.clientX - pointerStart.x;
|
||||
const swipeDirections = swipeDirectionsProp ?? getDefaultSwipeDirections(position);
|
||||
// Determine swipe direction if not already locked
|
||||
if (!swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
|
||||
swipeDirection = Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y';
|
||||
}
|
||||
let swipeAmount = { x: 0, y: 0 };
|
||||
if (swipeDirection === 'y') {
|
||||
// handle vertical swipes
|
||||
if (swipeDirections.includes('top') ||
|
||||
swipeDirections.includes('bottom')) {
|
||||
if ((swipeDirections.includes('top') && yDelta < 0) ||
|
||||
(swipeDirections.includes('bottom') && yDelta > 0)) {
|
||||
swipeAmount.y = yDelta;
|
||||
}
|
||||
else {
|
||||
// smoothly transition to dampened movement
|
||||
const dampenedDelta = yDelta * getDampening(yDelta);
|
||||
// ensure we don't jump when transition to dampened movement
|
||||
swipeAmount.y =
|
||||
Math.abs(dampenedDelta) < Math.abs(yDelta)
|
||||
? dampenedDelta
|
||||
: yDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (swipeDirection === 'x') {
|
||||
// handle horizontal swipes
|
||||
if (swipeDirections.includes('left') ||
|
||||
swipeDirections.includes('right')) {
|
||||
if ((swipeDirections.includes('left') && xDelta < 0) ||
|
||||
(swipeDirections.includes('right') && xDelta > 0)) {
|
||||
swipeAmount.x = xDelta;
|
||||
}
|
||||
else {
|
||||
// Smoothly transition to dampened movement
|
||||
const dampenedDelta = xDelta * getDampening(xDelta);
|
||||
// Ensure we don't jump when transitioning to dampened movement
|
||||
swipeAmount.x =
|
||||
Math.abs(dampenedDelta) < Math.abs(xDelta)
|
||||
? dampenedDelta
|
||||
: xDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
|
||||
isSwiped = true;
|
||||
}
|
||||
toastRef?.style.setProperty('--swipe-amount-x', `${swipeAmount.x}px`);
|
||||
toastRef?.style.setProperty('--swipe-amount-y', `${swipeAmount.y}px`);
|
||||
};
|
||||
const handleDragEnd = () => {
|
||||
swiping = false;
|
||||
swipeDirection = null;
|
||||
pointerStart = null;
|
||||
};
|
||||
const icon = $derived.by(() => {
|
||||
if (toast.icon)
|
||||
return toast.icon;
|
||||
if (toastType === 'success')
|
||||
return successIcon;
|
||||
if (toastType === 'error')
|
||||
return errorIcon;
|
||||
if (toastType === 'warning')
|
||||
return warningIcon;
|
||||
if (toastType === 'info')
|
||||
return infoIcon;
|
||||
if (toastType === 'loading')
|
||||
return loadingIcon;
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet LoadingIcon()}
|
||||
{#if loadingIcon}
|
||||
<div
|
||||
class={cn(classes?.loader, toast?.classes?.loader, 'sonner-loader')}
|
||||
data-visible={toastType === 'loading'}
|
||||
>
|
||||
{@render loadingIcon()}
|
||||
</div>
|
||||
{:else}
|
||||
<Loader
|
||||
class={cn(classes?.loader, toast.classes?.loader)}
|
||||
visible={toastType === 'loading'}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<li
|
||||
tabindex={0}
|
||||
bind:this={toastRef}
|
||||
class={cn(
|
||||
restProps.class,
|
||||
toastClass,
|
||||
classes?.toast,
|
||||
toast?.classes?.toast,
|
||||
classes?.[toastType],
|
||||
toast?.classes?.[toastType]
|
||||
)}
|
||||
aria-live={toast.important ? 'assertive' : 'polite'}
|
||||
aria-atomic="true"
|
||||
data-sonner-toast=""
|
||||
data-rich-colors={toast.richColors ?? defaultRichColors}
|
||||
data-styled={!(toast.component || toast.unstyled || unstyled)}
|
||||
data-mounted={mounted}
|
||||
data-promise={Boolean(toast.promise)}
|
||||
data-swiped={isSwiped}
|
||||
data-removed={removed}
|
||||
data-visible={isVisible}
|
||||
data-y-position={coords[0]}
|
||||
data-x-position={coords[1]}
|
||||
data-index={index}
|
||||
data-front={isFront}
|
||||
data-swiping={swiping}
|
||||
data-dismissible={dismissible}
|
||||
data-type={toastType}
|
||||
data-invert={invert}
|
||||
data-swipe-out={swipeOut}
|
||||
data-swipe-direction={swipeOutDirection}
|
||||
data-expanded={Boolean(expanded || (expandByDefault && mounted))}
|
||||
style:--index={index}
|
||||
style:--toasts-before={index}
|
||||
style:--z-index={toastState.toasts.length - index}
|
||||
style:--offset={`${removed ? offsetBeforeRemove : offset}px`}
|
||||
style:--initial-height={expandByDefault ? 'auto' : `${initialHeight}px`}
|
||||
style={`${restProps.style} ${toast.style}`}
|
||||
onpointermove={handlePointerMove}
|
||||
onpointerup={handlePointerUp}
|
||||
onpointerdown={handlePointerDown}
|
||||
ondragend={handleDragEnd}
|
||||
>
|
||||
{#if closeButton && !toast.component && toastType !== 'loading' && closeIcon !== null}
|
||||
<button
|
||||
aria-label={closeButtonAriaLabel}
|
||||
data-disabled={disabled}
|
||||
data-close-button
|
||||
onclick={() => {
|
||||
if (disabled || !dismissible) return;
|
||||
deleteToast();
|
||||
toast.onDismiss?.(toast);
|
||||
}}
|
||||
class={cn(classes?.closeButton, toast?.classes?.closeButton)}
|
||||
>
|
||||
{@render closeIcon?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if toast.component}
|
||||
{@const Component = toast.component}
|
||||
<Component {...toast.componentProps} closeToast={deleteToast} />
|
||||
{:else}
|
||||
{#if (toastType || toast.icon || toast.promise) && toast.icon !== null && (icon !== null || toast.icon)}
|
||||
<div data-icon="" class={cn(classes?.icon, toast?.classes?.icon)}>
|
||||
{#if toast.promise || toastType === 'loading'}
|
||||
{#if toast.icon}
|
||||
<toast.icon />
|
||||
{:else}
|
||||
{@render LoadingIcon()}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if toast.type !== 'loading'}
|
||||
{#if toast.icon}
|
||||
<toast.icon />
|
||||
{:else if toastType === 'success'}
|
||||
{@render successIcon?.()}
|
||||
{:else if toastType === 'error'}
|
||||
{@render errorIcon?.()}
|
||||
{:else if toastType === 'warning'}
|
||||
{@render warningIcon?.()}
|
||||
{:else if toastType === 'info'}
|
||||
{@render infoIcon?.()}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div data-content="" class={cn(classes?.content, toast?.classes?.content)}>
|
||||
<div
|
||||
data-title=""
|
||||
class={cn(classes?.title, toast?.classes?.title)}
|
||||
>
|
||||
{#if toast.title}
|
||||
{#if typeof toast.title !== 'string'}
|
||||
{@const Title = toast.title}
|
||||
<Title {...toast.componentProps} />
|
||||
{:else}
|
||||
{toast.title}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if toast.description}
|
||||
<div
|
||||
data-description=""
|
||||
class={cn(
|
||||
descriptionClass,
|
||||
toastDescriptionClass,
|
||||
classes?.description,
|
||||
toast.classes?.description
|
||||
)}
|
||||
>
|
||||
{#if typeof toast.description !== 'string'}
|
||||
{@const Description = toast.description}
|
||||
<Description {...toast.componentProps} />
|
||||
{:else}
|
||||
{toast.description}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if toast.cancel}
|
||||
{#if typeof toast.cancel === 'function'}
|
||||
<toast.cancel />
|
||||
{:else if isAction(toast.cancel)}
|
||||
<button
|
||||
data-button
|
||||
data-cancel
|
||||
style={toast.cancelButtonStyle ?? cancelButtonStyle}
|
||||
class={cn(
|
||||
classes?.cancelButton,
|
||||
toast?.classes?.cancelButton
|
||||
)}
|
||||
onclick={(event) => {
|
||||
if (!isAction(toast.cancel)) return;
|
||||
if (!dismissible) return;
|
||||
toast.cancel?.onClick?.(event);
|
||||
deleteToast();
|
||||
}}
|
||||
>
|
||||
{toast.cancel.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if toast.action}
|
||||
{#if typeof toast.action === 'function'}
|
||||
<toast.action />
|
||||
{:else if isAction(toast.action)}
|
||||
<button
|
||||
data-button=""
|
||||
style={toast.actionButtonStyle ?? actionButtonStyle}
|
||||
class={cn(
|
||||
classes?.actionButton,
|
||||
toast?.classes?.actionButton
|
||||
)}
|
||||
onclick={(event) => {
|
||||
if (!isAction(toast.action)) return;
|
||||
toast.action?.onClick(event);
|
||||
if (event.defaultPrevented) return;
|
||||
deleteToast();
|
||||
}}
|
||||
>
|
||||
{toast.action.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</li>
|
||||
Reference in New Issue
Block a user