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.
361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
import { attachRef, boxWith, onDestroyEffect, } from "svelte-toolbelt";
|
|
import { Context, watch } from "runed";
|
|
import { createBitsAttrs, boolToStr, getDataOpenClosed, boolToEmptyStrOrUndef, getDataTransitionAttrs, } from "../../internal/attrs.js";
|
|
import { kbd } from "../../internal/kbd.js";
|
|
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
|
|
const dialogAttrs = createBitsAttrs({
|
|
component: "dialog",
|
|
parts: ["content", "trigger", "overlay", "title", "description", "close", "cancel", "action"],
|
|
});
|
|
const DialogRootContext = new Context("Dialog.Root | AlertDialog.Root");
|
|
export class DialogRootState {
|
|
static create(opts) {
|
|
const parent = DialogRootContext.getOr(null);
|
|
return DialogRootContext.set(new DialogRootState(opts, parent));
|
|
}
|
|
opts;
|
|
triggerNode = $state(null);
|
|
contentNode = $state(null);
|
|
overlayNode = $state(null);
|
|
descriptionNode = $state(null);
|
|
contentId = $state(undefined);
|
|
titleId = $state(undefined);
|
|
triggerId = $state(undefined);
|
|
descriptionId = $state(undefined);
|
|
cancelNode = $state(null);
|
|
nestedOpenCount = $state(0);
|
|
depth;
|
|
parent;
|
|
contentPresence;
|
|
overlayPresence;
|
|
constructor(opts, parent) {
|
|
this.opts = opts;
|
|
this.parent = parent;
|
|
this.depth = parent ? parent.depth + 1 : 0;
|
|
this.handleOpen = this.handleOpen.bind(this);
|
|
this.handleClose = this.handleClose.bind(this);
|
|
this.contentPresence = new PresenceManager({
|
|
ref: boxWith(() => this.contentNode),
|
|
open: this.opts.open,
|
|
enabled: true,
|
|
onComplete: () => {
|
|
this.opts.onOpenChangeComplete.current(this.opts.open.current);
|
|
},
|
|
});
|
|
this.overlayPresence = new PresenceManager({
|
|
ref: boxWith(() => this.overlayNode),
|
|
open: this.opts.open,
|
|
enabled: true,
|
|
});
|
|
watch(() => this.opts.open.current, (isOpen) => {
|
|
if (!this.parent)
|
|
return;
|
|
if (isOpen) {
|
|
this.parent.incrementNested();
|
|
}
|
|
else {
|
|
this.parent.decrementNested();
|
|
}
|
|
}, { lazy: true });
|
|
onDestroyEffect(() => {
|
|
if (this.opts.open.current) {
|
|
this.parent?.decrementNested();
|
|
}
|
|
});
|
|
}
|
|
handleOpen() {
|
|
if (this.opts.open.current)
|
|
return;
|
|
this.opts.open.current = true;
|
|
}
|
|
handleClose() {
|
|
if (!this.opts.open.current)
|
|
return;
|
|
this.opts.open.current = false;
|
|
}
|
|
getBitsAttr = (part) => {
|
|
return dialogAttrs.getAttr(part, this.opts.variant.current);
|
|
};
|
|
incrementNested() {
|
|
this.nestedOpenCount++;
|
|
this.parent?.incrementNested();
|
|
}
|
|
decrementNested() {
|
|
if (this.nestedOpenCount === 0)
|
|
return;
|
|
this.nestedOpenCount--;
|
|
this.parent?.decrementNested();
|
|
}
|
|
sharedProps = $derived.by(() => ({
|
|
"data-state": getDataOpenClosed(this.opts.open.current),
|
|
}));
|
|
}
|
|
export class DialogTriggerState {
|
|
static create(opts) {
|
|
return new DialogTriggerState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(this.opts.ref, (v) => {
|
|
this.root.triggerNode = v;
|
|
this.root.triggerId = v?.id;
|
|
});
|
|
this.onclick = this.onclick.bind(this);
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
}
|
|
onclick(e) {
|
|
if (this.opts.disabled.current)
|
|
return;
|
|
if (e.button > 0)
|
|
return;
|
|
this.root.handleOpen();
|
|
}
|
|
onkeydown(e) {
|
|
if (this.opts.disabled.current)
|
|
return;
|
|
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
|
e.preventDefault();
|
|
this.root.handleOpen();
|
|
}
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
"aria-haspopup": "dialog",
|
|
"aria-expanded": boolToStr(this.root.opts.open.current),
|
|
"aria-controls": this.root.contentId,
|
|
[this.root.getBitsAttr("trigger")]: "",
|
|
onkeydown: this.onkeydown,
|
|
onclick: this.onclick,
|
|
disabled: this.opts.disabled.current ? true : undefined,
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DialogCloseState {
|
|
static create(opts) {
|
|
return new DialogCloseState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(this.opts.ref);
|
|
this.onclick = this.onclick.bind(this);
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
}
|
|
onclick(e) {
|
|
if (this.opts.disabled.current)
|
|
return;
|
|
if (e.button > 0)
|
|
return;
|
|
this.root.handleClose();
|
|
}
|
|
onkeydown(e) {
|
|
if (this.opts.disabled.current)
|
|
return;
|
|
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
|
e.preventDefault();
|
|
this.root.handleClose();
|
|
}
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
[this.root.getBitsAttr(this.opts.variant.current)]: "",
|
|
onclick: this.onclick,
|
|
onkeydown: this.onkeydown,
|
|
disabled: this.opts.disabled.current ? true : undefined,
|
|
tabindex: 0,
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DialogActionState {
|
|
static create(opts) {
|
|
return new DialogActionState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(this.opts.ref);
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
[this.root.getBitsAttr("action")]: "",
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DialogTitleState {
|
|
static create(opts) {
|
|
return new DialogTitleState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.root.titleId = this.opts.id.current;
|
|
this.attachment = attachRef(this.opts.ref);
|
|
watch.pre(() => this.opts.id.current, (id) => {
|
|
this.root.titleId = id;
|
|
});
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
role: "heading",
|
|
"aria-level": this.opts.level.current,
|
|
[this.root.getBitsAttr("title")]: "",
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DialogDescriptionState {
|
|
static create(opts) {
|
|
return new DialogDescriptionState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.root.descriptionId = this.opts.id.current;
|
|
this.attachment = attachRef(this.opts.ref, (v) => {
|
|
this.root.descriptionNode = v;
|
|
});
|
|
watch.pre(() => this.opts.id.current, (id) => {
|
|
this.root.descriptionId = id;
|
|
});
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
[this.root.getBitsAttr("description")]: "",
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
}
|
|
export class DialogContentState {
|
|
static create(opts) {
|
|
return new DialogContentState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(this.opts.ref, (v) => {
|
|
this.root.contentNode = v;
|
|
this.root.contentId = v?.id;
|
|
});
|
|
}
|
|
snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
role: this.root.opts.variant.current === "alert-dialog" ? "alertdialog" : "dialog",
|
|
"aria-modal": "true",
|
|
"aria-describedby": this.root.descriptionId,
|
|
"aria-labelledby": this.root.titleId,
|
|
[this.root.getBitsAttr("content")]: "",
|
|
style: {
|
|
pointerEvents: "auto",
|
|
outline: this.root.opts.variant.current === "alert-dialog" ? "none" : undefined,
|
|
"--bits-dialog-depth": this.root.depth,
|
|
"--bits-dialog-nested-count": this.root.nestedOpenCount,
|
|
// CSS containment isolates style/layout calculations from the rest of the page,
|
|
// improving performance when there's a large DOM behind the dialog.
|
|
// Paint is omitted so tooltips/selects can render outside dialog bounds.
|
|
contain: "layout style",
|
|
},
|
|
tabindex: this.root.opts.variant.current === "alert-dialog" ? -1 : undefined,
|
|
"data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
|
|
"data-nested": boolToEmptyStrOrUndef(this.root.parent !== null),
|
|
...getDataTransitionAttrs(this.root.contentPresence.transitionStatus),
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
get shouldRender() {
|
|
return this.root.contentPresence.shouldRender;
|
|
}
|
|
}
|
|
export class DialogOverlayState {
|
|
static create(opts) {
|
|
return new DialogOverlayState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(this.opts.ref, (v) => (this.root.overlayNode = v));
|
|
}
|
|
snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
[this.root.getBitsAttr("overlay")]: "",
|
|
style: {
|
|
pointerEvents: "auto",
|
|
"--bits-dialog-depth": this.root.depth,
|
|
"--bits-dialog-nested-count": this.root.nestedOpenCount,
|
|
},
|
|
"data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
|
|
"data-nested": boolToEmptyStrOrUndef(this.root.parent !== null),
|
|
...getDataTransitionAttrs(this.root.overlayPresence.transitionStatus),
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
get shouldRender() {
|
|
return this.root.overlayPresence.shouldRender;
|
|
}
|
|
}
|
|
export class AlertDialogCancelState {
|
|
static create(opts) {
|
|
return new AlertDialogCancelState(opts, DialogRootContext.get());
|
|
}
|
|
opts;
|
|
root;
|
|
attachment;
|
|
constructor(opts, root) {
|
|
this.opts = opts;
|
|
this.root = root;
|
|
this.attachment = attachRef(this.opts.ref, (v) => (this.root.cancelNode = v));
|
|
this.onclick = this.onclick.bind(this);
|
|
this.onkeydown = this.onkeydown.bind(this);
|
|
}
|
|
onclick(e) {
|
|
if (this.opts.disabled.current)
|
|
return;
|
|
if (e.button > 0)
|
|
return;
|
|
this.root.handleClose();
|
|
}
|
|
onkeydown(e) {
|
|
if (this.opts.disabled.current)
|
|
return;
|
|
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
|
e.preventDefault();
|
|
this.root.handleClose();
|
|
}
|
|
}
|
|
props = $derived.by(() => ({
|
|
id: this.opts.id.current,
|
|
[this.root.getBitsAttr("cancel")]: "",
|
|
onclick: this.onclick,
|
|
onkeydown: this.onkeydown,
|
|
tabindex: 0,
|
|
...this.root.sharedProps,
|
|
...this.attachment,
|
|
}));
|
|
}
|