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:
264
web/node_modules/bits-ui/dist/bits/pagination/pagination.svelte.js
generated
vendored
Normal file
264
web/node_modules/bits-ui/dist/bits/pagination/pagination.svelte.js
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
import { attachRef } from "svelte-toolbelt";
|
||||
import { Context } from "runed";
|
||||
import { createBitsAttrs } from "../../internal/attrs.js";
|
||||
import { getElemDirection } from "../../internal/locale.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { getDirectionalKeys } from "../../internal/get-directional-keys.js";
|
||||
import { useId } from "../../shared/index.js";
|
||||
const paginationAttrs = createBitsAttrs({
|
||||
component: "pagination",
|
||||
parts: ["root", "page", "prev", "next"],
|
||||
});
|
||||
const PaginationRootContext = new Context("Pagination.Root");
|
||||
export class PaginationRootState {
|
||||
static create(opts) {
|
||||
return PaginationRootContext.set(new PaginationRootState(opts));
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
totalPages = $derived.by(() => {
|
||||
if (this.opts.count.current === 0)
|
||||
return 1;
|
||||
return Math.ceil(this.opts.count.current / this.opts.perPage.current);
|
||||
});
|
||||
range = $derived.by(() => {
|
||||
const start = (this.opts.page.current - 1) * this.opts.perPage.current;
|
||||
const end = Math.min(start + this.opts.perPage.current, this.opts.count.current);
|
||||
return { start: start + 1, end };
|
||||
});
|
||||
pages = $derived.by(() => getPageItems({
|
||||
page: this.opts.page.current,
|
||||
totalPages: this.totalPages,
|
||||
siblingCount: this.opts.siblingCount.current,
|
||||
}));
|
||||
hasPrevPage = $derived.by(() => this.opts.page.current > 1);
|
||||
hasNextPage = $derived.by(() => this.opts.page.current < this.totalPages);
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
setPage(page) {
|
||||
this.opts.page.current = page;
|
||||
}
|
||||
getPageTriggerNodes() {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return [];
|
||||
return Array.from(node.querySelectorAll("[data-pagination-page]"));
|
||||
}
|
||||
getButtonNode(type) {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return;
|
||||
return node.querySelector(paginationAttrs.selector(type));
|
||||
}
|
||||
prevPage() {
|
||||
this.opts.page.current = Math.max(this.opts.page.current - 1, 1);
|
||||
}
|
||||
nextPage() {
|
||||
this.opts.page.current = Math.min(this.opts.page.current + 1, this.totalPages);
|
||||
}
|
||||
snippetProps = $derived.by(() => ({
|
||||
pages: this.pages,
|
||||
range: this.range,
|
||||
currentPage: this.opts.page.current,
|
||||
}));
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
[paginationAttrs.root]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class PaginationPageState {
|
||||
static create(opts) {
|
||||
return new PaginationPageState(opts, PaginationRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
#isSelected = $derived.by(() => this.opts.page.current.value === this.root.opts.page.current);
|
||||
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.setPage(this.opts.page.current.value);
|
||||
}
|
||||
onkeydown(e) {
|
||||
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
||||
e.preventDefault();
|
||||
this.root.setPage(this.opts.page.current.value);
|
||||
}
|
||||
else {
|
||||
handleTriggerKeydown(e, this.opts.ref.current, this.root);
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"aria-label": `Page ${this.opts.page.current.value}`,
|
||||
"data-value": `${this.opts.page.current.value}`,
|
||||
"data-selected": this.#isSelected ? "" : undefined,
|
||||
[paginationAttrs.page]: "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class PaginationButtonState {
|
||||
static create(opts) {
|
||||
return new PaginationButtonState(opts, PaginationRootContext.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);
|
||||
}
|
||||
#action() {
|
||||
this.opts.type === "prev" ? this.root.prevPage() : this.root.nextPage();
|
||||
}
|
||||
#isDisabled = $derived.by(() => {
|
||||
if (this.opts.disabled.current)
|
||||
return true;
|
||||
if (this.opts.type === "prev")
|
||||
return !this.root.hasPrevPage;
|
||||
if (this.opts.type === "next")
|
||||
return !this.root.hasNextPage;
|
||||
return false;
|
||||
});
|
||||
onclick(e) {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
this.#action();
|
||||
}
|
||||
onkeydown(e) {
|
||||
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
||||
e.preventDefault();
|
||||
this.#action();
|
||||
}
|
||||
else {
|
||||
handleTriggerKeydown(e, this.opts.ref.current, this.root);
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
[paginationAttrs[this.opts.type]]: "",
|
||||
disabled: this.#isDisabled,
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
//
|
||||
// HELPERS
|
||||
//
|
||||
/**
|
||||
* Shared logic for handling keyboard navigation on
|
||||
* pagination page triggers and prev/next buttons.
|
||||
*
|
||||
*
|
||||
* @param e - KeyboardEvent
|
||||
* @param node - The HTMLElement that triggered the event.
|
||||
* @param root - The root pagination state instance
|
||||
*/
|
||||
function handleTriggerKeydown(e, node, root) {
|
||||
if (!node || !root.opts.ref.current)
|
||||
return;
|
||||
const items = root.getPageTriggerNodes();
|
||||
const nextButton = root.getButtonNode("next");
|
||||
const prevButton = root.getButtonNode("prev");
|
||||
if (prevButton) {
|
||||
items.unshift(prevButton);
|
||||
}
|
||||
if (nextButton) {
|
||||
items.push(nextButton);
|
||||
}
|
||||
const currentIndex = items.indexOf(node);
|
||||
const dir = getElemDirection(root.opts.ref.current);
|
||||
const { nextKey, prevKey } = getDirectionalKeys(dir, root.opts.orientation.current);
|
||||
const loop = root.opts.loop.current;
|
||||
const keyToIndex = {
|
||||
[nextKey]: currentIndex + 1,
|
||||
[prevKey]: currentIndex - 1,
|
||||
[kbd.HOME]: 0,
|
||||
[kbd.END]: items.length - 1,
|
||||
};
|
||||
let itemIndex = keyToIndex[e.key];
|
||||
if (itemIndex === undefined)
|
||||
return;
|
||||
e.preventDefault();
|
||||
if (itemIndex < 0 && loop) {
|
||||
itemIndex = items.length - 1;
|
||||
}
|
||||
else if (itemIndex === items.length && loop) {
|
||||
itemIndex = 0;
|
||||
}
|
||||
const itemToFocus = items[itemIndex];
|
||||
if (!itemToFocus)
|
||||
return;
|
||||
itemToFocus.focus();
|
||||
}
|
||||
/**
|
||||
* Returns an array of page items used to render out the
|
||||
* pagination page triggers.
|
||||
*
|
||||
* Credit: https://github.com/melt-ui/melt-ui
|
||||
*/
|
||||
function getPageItems({ page = 1, totalPages, siblingCount = 1 }) {
|
||||
const pageItems = [];
|
||||
const pagesToShow = new Set([1, totalPages]);
|
||||
const firstItemWithSiblings = 3 + siblingCount;
|
||||
const lastItemWithSiblings = totalPages - 2 - siblingCount;
|
||||
if (firstItemWithSiblings > lastItemWithSiblings) {
|
||||
for (let i = 2; i <= totalPages - 1; i++) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
else if (page < firstItemWithSiblings) {
|
||||
for (let i = 2; i <= Math.min(firstItemWithSiblings, totalPages); i++) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
else if (page > lastItemWithSiblings) {
|
||||
for (let i = totalPages - 1; i >= Math.max(lastItemWithSiblings, 2); i--) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = Math.max(page - siblingCount, 2); i <= Math.min(page + siblingCount, totalPages); i++) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
function addPage(value) {
|
||||
pageItems.push({ type: "page", value, key: `page-${value}` });
|
||||
}
|
||||
function addEllipsis() {
|
||||
const id = useId();
|
||||
pageItems.push({ type: "ellipsis", key: `ellipsis-${id}` });
|
||||
}
|
||||
let lastNumber = 0;
|
||||
for (const p of Array.from(pagesToShow).sort((a, b) => a - b)) {
|
||||
if (p - lastNumber > 1) {
|
||||
addEllipsis();
|
||||
}
|
||||
addPage(p);
|
||||
lastNumber = p;
|
||||
}
|
||||
return pageItems;
|
||||
}
|
||||
Reference in New Issue
Block a user