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.
227 lines
7.2 KiB
JavaScript
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);
|
|
}
|
|
});
|