Files
oikos/web/node_modules/svelte-sonner/dist/toast-state.svelte.js
dtoro d4d99a7473
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
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.
2026-08-15 22:27:52 +02:00

227 lines
7.2 KiB
JavaScript

import { isBrowser } from './internal/helpers.js';
import { sonnerContext } from './internal/ctx.js';
import { untrack } from 'svelte';
let toastsCounter = 0;
class ToastState {
toasts = $state([]);
heights = $state([]);
#findToastIdx = (id) => {
const idx = this.toasts.findIndex((toast) => toast.id === id);
if (idx === -1)
return null;
return idx;
};
addToast = (data) => {
if (!isBrowser)
return;
this.toasts.unshift(data);
};
updateToast = ({ id, data, type, message }) => {
const toastIdx = this.toasts.findIndex((toast) => toast.id === id);
const toastToUpdate = this.toasts[toastIdx];
this.toasts[toastIdx] = {
...toastToUpdate,
...data,
id,
title: message,
type,
updated: true
};
};
create = (data) => {
const { message, ...rest } = data;
const id = typeof data?.id === 'number' || (data.id && data.id?.length > 0)
? data.id
: toastsCounter++;
// Support deprecated `dismissable` as a fallback for backwards compatibility
const dismissible = data.dismissible !== undefined
? data.dismissible
: data.dismissable !== undefined
? data.dismissable
: true;
const type = data.type === undefined ? 'default' : data.type;
untrack(() => {
const alreadyExists = this.toasts.find((toast) => toast.id === id);
if (alreadyExists) {
this.updateToast({ id, data, type, message, dismissible });
}
else {
this.addToast({ ...rest, id, title: message, dismissible, type });
}
});
return id;
};
dismiss = (id) => {
untrack(() => {
if (id === undefined) {
// we're dismissing all the toasts
this.toasts = this.toasts.map((toast) => ({ ...toast, dismiss: true }));
return;
}
// we're dismissing a specific toast
const toastIdx = this.toasts.findIndex((toast) => toast.id === id);
if (this.toasts[toastIdx]) {
this.toasts[toastIdx] = { ...this.toasts[toastIdx], dismiss: true };
}
});
return id;
};
remove = (id) => {
if (id === undefined) {
// remove all toasts
this.toasts = [];
return;
}
// remove a specific toast
const toastIdx = this.#findToastIdx(id);
if (toastIdx === null)
return;
this.toasts.splice(toastIdx, 1);
return id;
};
message = (message, data) => {
return this.create({ ...data, type: 'default', message });
};
error = (message, data) => {
return this.create({ ...data, type: 'error', message });
};
success = (message, data) => {
return this.create({ ...data, type: 'success', message });
};
info = (message, data) => {
return this.create({ ...data, type: 'info', message });
};
warning = (message, data) => {
return this.create({ ...data, type: 'warning', message });
};
loading = (message, data) => {
return this.create({ ...data, type: 'loading', message });
};
promise = (promise, data) => {
if (!data) {
// Nothing to show
return;
}
let id = undefined;
if (data.loading !== undefined) {
id = this.create({
...data,
promise,
type: 'loading',
message: typeof data.loading === 'string' ? data.loading : data.loading()
});
}
const p = promise instanceof Promise ? promise : promise();
let shouldDismiss = id !== undefined;
p.then((response) => {
if (typeof response === 'object' &&
response &&
'ok' in response &&
typeof response.ok === 'boolean' &&
!response.ok) {
shouldDismiss = false;
const message = constructPromiseErrorMessage(response);
this.create({ id, type: 'error', message });
}
else if (data.success !== undefined) {
shouldDismiss = false;
const message = typeof data.success === 'function' ? data.success(response) : data.success;
this.create({ id, type: 'success', message });
}
})
.catch((error) => {
if (data.error !== undefined) {
shouldDismiss = false;
const message = typeof data.error === 'function' ? data.error(error) : data.error;
this.create({ id, type: 'error', message });
}
})
.finally(() => {
if (shouldDismiss) {
// Toast is still in load state (and will be indefinitely — dismiss it)
this.dismiss(id);
id = undefined;
}
data.finally?.();
});
return id;
};
custom = (component, data) => {
const id = data?.id || toastsCounter++;
this.create({ component, id, ...data });
return id;
};
removeHeight = (id) => {
this.heights = this.heights.filter((height) => height.toastId !== id);
};
setHeight = (data) => {
const toastIdx = this.#findToastIdx(data.toastId);
if (toastIdx === null) {
this.heights.push(data);
return;
}
this.heights[toastIdx] = data;
};
reset = () => {
this.toasts = [];
this.heights = [];
};
}
function constructPromiseErrorMessage(response) {
if (response && typeof response === 'object' && 'status' in response) {
return `HTTP error! Status: ${response.status}`;
}
return `Error! ${response}`;
}
export const toastState = new ToastState();
function toastFunction(message, data) {
return toastState.create({
message,
...data
});
}
export class SonnerState {
/**
* A derived state of the toasts that are not dismissed.
*/
#activeToasts = $derived(toastState.toasts.filter((toast) => !toast.dismiss));
get toasts() {
return this.#activeToasts;
}
}
/**
* A hook to get a reference to the sonner toast state.
*
* Returns a class instance a getter for the `toasts` array.
*
* @example
* ```svelte
* <script lang="ts">
* import { useSonner } from 'svelte-sonner';
*
* const sonner = useSonner();
*
* // Reactive access to the toasts
* $effect(() => console.log(sonner.toasts))
* </script>
* ```
*/
export function useSonner() {
return sonnerContext.get();
}
const basicToast = toastFunction;
export const toast = Object.assign(basicToast, {
success: toastState.success,
info: toastState.info,
warning: toastState.warning,
error: toastState.error,
custom: toastState.custom,
message: toastState.message,
promise: toastState.promise,
dismiss: toastState.dismiss,
loading: toastState.loading,
getActiveToasts: () => {
return toastState.toasts.filter((toast) => !toast.dismiss);
}
});