diff --git a/baidupcs-go/Makefile b/baidupcs-go/Makefile index 6f7194910..76ac5c171 100644 --- a/baidupcs-go/Makefile +++ b/baidupcs-go/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=BaiduPCS-Go PKG_VERSION:=4.0.2 -PKG_RELEASE:=8 +PKG_RELEASE:=9 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/qjfoidnh/BaiduPCS-Go/tar.gz/v$(PKG_VERSION)? diff --git a/baidupcs-go/patches/010-fix-go-runtime-linkname.patch b/baidupcs-go/patches/010-fix-go-runtime-linkname.patch new file mode 100644 index 000000000..8aa119783 --- /dev/null +++ b/baidupcs-go/patches/010-fix-go-runtime-linkname.patch @@ -0,0 +1,25 @@ +From: coolsnowwolf +Subject: [PATCH] cachepool: stop using runtime.rawbyteslice + +Recent Go releases reject external linkname references to rawbyteslice. +Use the language-provided slice allocator for the zeroed allocation path. + +Signed-off-by: coolsnowwolf +--- +--- a/pcsutil/cachepool/malloc.go ++++ b/pcsutil/cachepool/malloc.go +@@ -8,12 +8,9 @@ + //go:linkname mallocgc runtime.mallocgc + func mallocgc(size uintptr, typ uintptr, needzero bool) unsafe.Pointer + +-//go:linkname rawbyteslice runtime.rawbyteslice +-func rawbyteslice(size int) (b []byte) +- +-// RawByteSlice point to runtime.rawbyteslice ++// RawByteSlice allocates a zeroed byte slice. + func RawByteSlice(size int) (b []byte) { +- return rawbyteslice(size) ++ return make([]byte, size) + } + + // RawMalloc allocates a new slice. The slice is not zeroed. diff --git a/filebrowser/Makefile b/filebrowser/Makefile index f05e1b87e..a2dfce2b0 100644 --- a/filebrowser/Makefile +++ b/filebrowser/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser -PKG_VERSION:=1.5.4-stable +PKG_VERSION:=1.5.5-stable PKG_RELEASE=1 ifeq ($(ARCH),aarch64) diff --git a/geth/Makefile b/geth/Makefile index c09130ab1..fc59ba9dd 100644 --- a/geth/Makefile +++ b/geth/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=go-ethereum PKG_VERSION:=1.17.5 -PKG_RELEASE:=8 +PKG_RELEASE:=9 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/ethereum/go-ethereum/tar.gz/v${PKG_VERSION}? @@ -26,6 +26,7 @@ PKG_CONFIG_DEPENDS:=CONFIG_BUILD_NLS GO_PKG:=github.com/ethereum/go-ethereum GO_PKG_BUILD_PKG:=github.com/ethereum/go-ethereum/cmd/geth +GO_PKG_TAGS:=untested_go_version include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/nls.mk diff --git a/geth/files/geth.init b/geth/files/geth.init index 154c11fcb..ec61c18c4 100644 --- a/geth/files/geth.init +++ b/geth/files/geth.init @@ -9,7 +9,7 @@ PROG=/usr/bin/geth start_service() { procd_open_instance procd_set_param command ${PROG} - procd_append_param command --syncmode "light" --cache 1024 + procd_append_param command --syncmode "snap" --cache 1024 procd_set_param respawn procd_close_instance } diff --git a/headscale/Makefile b/headscale/Makefile index 6aca2fc98..05593d22b 100644 --- a/headscale/Makefile +++ b/headscale/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=headscale PKG_VERSION:=0.29.3 -PKG_RELEASE:=7 +PKG_RELEASE:=8 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)? @@ -24,7 +24,7 @@ GO_PKG:=github.com/juanfont/headscale GO_PKG_BUILD_PKG:=$(GO_PKG)/cmd/headscale GO_PKG_LDFLAGS:=-s -w GO_PKG_LDFLAGS+= \ - -X '$(GO_PKG_BUILD_PKG)/cli.Version=v$(PKG_VERSION)' + -X '$(GO_PKG)/hscontrol/types.version=v$(PKG_VERSION)' include $(INCLUDE_DIR)/package.mk include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk @@ -47,7 +47,7 @@ define Package/headscale/conffiles /etc/headscale/db.sqlite /etc/headscale/derp.yaml /etc/headscale/noise_private.key -/etc/headscale/private.key +/etc/headscale/derp_server_private.key endef define Package/headscale/install @@ -56,6 +56,7 @@ define Package/headscale/install $(INSTALL_DIR) $(1)/etc/headscale touch $(1)/etc/headscale/db.sqlite $(INSTALL_CONF) $(PKG_BUILD_DIR)/config-example.yaml $(1)/etc/headscale/config.yaml + $(SED) 's#/var/lib/headscale#/etc/headscale#g' $(1)/etc/headscale/config.yaml $(INSTALL_CONF) $(PKG_BUILD_DIR)/derp-example.yaml $(1)/etc/headscale/derp.yaml endef diff --git a/headscale/patches/010-report-packaged-version.patch b/headscale/patches/010-report-packaged-version.patch new file mode 100644 index 000000000..9a0fc91e8 --- /dev/null +++ b/headscale/patches/010-report-packaged-version.patch @@ -0,0 +1,14 @@ +Allow package builds from release tarballs to provide the version because Go +records the main module version as "(devel)" outside the module proxy. This +keeps Headscale's database upgrade checks enabled for packaged releases. + +Signed-off-by: coolsnowwolf +--- +--- a/hscontrol/types/version.go ++++ b/hscontrol/types/version.go +@@ -24,0 +25,2 @@ ++var version = "dev" ++ +@@ -45 +47 @@ +- Version: "dev", ++ Version: version, diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 91c06cde4..62fe88909 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=45 +PKG_RELEASE:=46 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index 6f12eee70..6c1b2f8b5 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -885,9 +885,20 @@ return baseclass.extend({ * theme/30-tables.css gives a data table an honest min-content floor for as long as it is a * table, so a starved column really does overflow. Do not reconstruct min-content in JS — a * canvas approximation cost ~1ms per pass on a 114-row table and claimed 144px where the - * engine's own floor is 93. */ + * engine's own floor is 93. + * + * TWO measurements, because a table overflows in two directions and `scrollWidth` only sees one. + * A `display: table` box does not clip: when min-content needs more than it was given it GROWS + * PAST its parent, so its scrollWidth and clientWidth rise together and the overflow is + * invisible from inside — the same trap `roomFor()` above is written around. The box's own + * width is what the reader sees sticking out, and it is what tools/live-audit.mjs measures + * (`right > host + 1.5`). Taking the larger of the two makes this test answer the question the + * gate asks: `#packages` on a fresh snapshot router came out 2px past the content column at + * 1440 and stayed un-carded, because scrollWidth alone said it fitted. */ overflows(el) { - return el.scrollWidth > this.roomFor(el) + 1; /* +1: sub-pixel rounding */ + const room = this.roomFor(el); + const grown = el.getBoundingClientRect().width; + return Math.max(el.scrollWidth, grown) > room + 1; /* +1: sub-pixel rounding */ } }); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js index a059edcfb..c7e870c91 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js @@ -159,6 +159,10 @@ function currentNode() { return baseclass.extend({ setTree, tree: () => _tree, + /* raw presence, no alias or firstchild resolution: fs-commands gates each command on the menu + * node whose `depends.acl` names the group that command needs, and a node that resolves + * elsewhere would answer for a permission the session may not hold */ + nodeForSegs, segsFromPath, currentNode, resolveSegs, diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js index a12b819cf..4951522e1 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js @@ -720,6 +720,35 @@ function commitStage(stage, contentHost) { dropStage(stage); } +/* ---- the commit is the one frame of a navigation worth animating ---- + * + * `commitStage()` is a synchronous DOM move, so a view transition here wraps a FRAME, not a render: + * the update callback settles in the same tick and the API's rendering suppression cannot outlive + * it. Wrapping navigate() instead would freeze the page for the whole chain — 136-196 ms median + * (docs/spa-router.md) and up to RENDER_TIMEOUT on a cold route. + * + * The scroll restore runs INSIDE the callback: `::view-transition` is a fixed overlay of the old + * pixels, and a scroll that lands after the snapshot slides the live page under a still image. + * + * Reduced motion is answered by not starting a transition at all, rather than by a zeroed animation: + * the `*` rule in theme/95-a11y-media.css does not reach a pseudo tree, and skipping also saves the + * snapshot. Read per navigation, so an OS change needs no listener. + * + * `ready` rejects when the transition is skipped — a hidden document, a duplicate name — which is a + * normal outcome and not an error to report; the swap itself has happened either way. + * + * BOTH promises are taken, and `finished` is not decoration: it rejects with whatever the callback + * threw, so leaving it alone turns one fault into two console lines — the throw itself and an + * unhandled rejection behind it. Measured on a page whose callback throws: two `pageerror`s with + * only `ready` handled, one with both. The extra line is noise in a log the live gates read. */ +function swapIn(commit) { + const mq = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)'); + if ((mq && mq.matches) || typeof document.startViewTransition !== 'function') { commit(); return; } + const t = document.startViewTransition(commit); + t.ready.catch(() => {}); + t.finished.catch(() => {}); +} + /* The `#view` the document keeps between navigations, i.e. the one the observers are bound to: * whichever `#view` is not the stage. A document that has none gets one, once. */ function liveView(contentHost, stage) { @@ -1072,9 +1101,11 @@ function navigate(pathname, push, kbd) { /* superseded while rendering: the chain painted into its own stage, so drop it and * leave the live page to the newer navigation */ if (gen !== _navGen) { dropStage(stage); return; } - commitStage(stage, contentHost); - /* now, and only now, is there one height to read: the incoming page's */ - if (restoreTo) restoreScroll(restoreTo, gen); + swapIn(() => { + commitStage(stage, contentHost); + /* now, and only now, is there one height to read: the incoming page's */ + if (restoreTo) restoreScroll(restoreTo, gen); + }); }) .catch((e) => { dropStage(stage); throw e; }); }).catch((e) => { diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js index 0fc1aea66..5d8a17156 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js @@ -99,6 +99,46 @@ function index() { return _index; } +/* ---- extra sources ------------------------------------------------------- + * + * An optional package can add rows to the same list — one indexes the SECTION titles inside each + * page, so "footstrap" finds System -> Appearance. A source hands over entries in the shape + * buildIndex() produces and nothing else: the matching, the ranking and the rendering stay here, + * or two lists would disagree about what a hit is. + * + * Registration is a GLOBAL ARRAY, not an export a package requires. This module is fetched on the + * first gesture and most sessions never make it; a package that had to `require` it to register + * would pull it onto every page and pay its 4.5 KB for a palette nobody opened. Pushing a function + * onto `window.__fsSearchSources` costs the package nothing and names no one in either direction. + * + * `window.__fsSearchGen` is how a source says its data grew — a harvester fills in over a session + * — and the stamp below is what rebuilds the pool when it does. */ +const _sources = []; +let _pool = null, _stamp = -1; + +function globalSources() { + return Array.isArray(window.__fsSearchSources) ? window.__fsSearchSources : []; +} + +function addSource(fn) { + _sources.push(fn); + _pool = null; +} +function refresh() { + _pool = null; +} +function pool() { + const all = _sources.concat(globalSources()); + const stamp = all.length + (window.__fsSearchGen || 0); + if (_pool && stamp === _stamp) return _pool; + _stamp = stamp; + _pool = all.reduce((rows, fn) => { + try { return rows.concat(fn() || []); } + catch (e) { console.error('footstrap: a search source threw', e); return rows; } + }, index().slice()); + return _pool; +} + /* ---- matching ----------------------------------------------------------- */ /* Every whitespace-separated token must hit something: a second word means AND. Deliberately not @@ -118,7 +158,7 @@ function search(q, limit) { if (!toks.length) return []; const hits = []; - for (const e of index()) { + for (const e of pool()) { let sum = 0; for (const tok of toks) { const s = tokenScore(e, tok); @@ -252,8 +292,16 @@ function build() { e.trail.length ? E('span', { 'class': 'fs-search-opt-path' }, [ e.trail.join(' › ') ]) : '' ]); /* close before the click reaches the router, which re-renders the chrome underneath; - * no focus return, the user is going elsewhere */ - a.addEventListener('click', () => close(false)); + * no focus return, the user is going elsewhere. + * + * `onTake` is how a row from an extra source finishes the job the href cannot: a + * section row's href can only reach the PAGE, so the source that produced it opens the + * tab and scrolls to the section itself. It fires for a click and for Enter alike — + * Enter synthesises this very click. */ + a.addEventListener('click', () => { + close(false); + if (typeof e.onTake === 'function') e.onTake(); + }); a.addEventListener('pointermove', () => { if (at !== i) setActive(i); }); list.appendChild(a); return a; @@ -354,5 +402,7 @@ function openPalette() { } return baseclass.extend({ - open: openPalette + open: openPalette, + /* the seam an optional package registers through; see addSource() */ + addSource, refresh }); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js b/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js index 60e9579b9..f5f654aaa 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js @@ -96,21 +96,25 @@ function wireSearch() { warmRecent(); /* One fetch, on the first gesture. The module builds its overlay and opens itself; every later - * gesture reaches the same instance, `require` being a singleton. */ - let pending = false; + * gesture reaches the same instance, `require` being a singleton. + * + * …and then this half stands down: fs-search binds its own toggle to the same button and its + * own copies of Ctrl+K and `/`, so while both were live the module's toggle closed the palette + * and this one re-opened it in the microtask after, and the button looked broken. */ + let pending = false, loaded = false; const open = () => { - if (pending) return; + if (pending || loaded) return; pending = true; - RT.require('fs-search').then((m) => { pending = false; m.open(); }, + RT.require('fs-search').then((m) => { pending = false; loaded = true; m.open(); }, (e) => { pending = false; console.error('footstrap: fs-search did not load', e); }); }; - btn.addEventListener('click', open); + btn.addEventListener('click', () => open()); /* the same two shortcuts the palette used to own, with the same guard: `/` must not steal a * keystroke from someone typing into a field, a contenteditable, or a .cbi-dropdown, where * fs-select.js's typeahead reads it as a search character */ document.addEventListener('keydown', (ev) => { - if (ev.defaultPrevented) return; + if (ev.defaultPrevented || loaded) return; if ((ev.ctrlKey || ev.metaKey) && !ev.altKey && (ev.key === 'k' || ev.key === 'K')) { ev.preventDefault(); open(); return; } @@ -120,6 +124,23 @@ function wireSearch() { }); } +/* ---- optional companion packages ---- + * + * header.ut prints `window.__fsPlugins` from `footstrap.settings.plugin`, a list a package writes + * from its own uci-defaults; each entry is a LuCI module name, already whitelisted there. The + * chrome requires each one after everything below is wired — a plugin registers itself through the + * seams the theme exports (`fs-router.onNavigate`, `fs-search.addSource`) and the theme names + * nobody. A plugin that throws costs only itself. + * + * No plugin, no cost: an empty list is the shipped state and this loop does nothing. */ +function loadPlugins() { + const RT = window.L; + const names = Array.isArray(window.__fsPlugins) ? window.__fsPlugins : []; + names.forEach((name) => { + RT.require(name).catch((e) => console.error('footstrap: plugin ' + name + ' did not load', e)); + }); +} + /* The three template globals Status -> Overview needs, defined where ordering is guaranteed. * * `admin_status/index.ut` defines `progressbar`, `renderBox` and `renderBadge` in an inline script @@ -227,6 +248,9 @@ return baseclass.extend({ wirePageModules(); router.wire(); router.wireVisibility(); + /* last: a plugin registers against the parts above, and a broken one must not be able + * to take the chrome with it */ + loadPlugins(); /* no sane partial recovery — a throw above loses the menu, the router and the Appearance * tab together — so this fails loudly rather than silently */ }).catch((e) => console.error('footstrap: chrome init failed', e)); diff --git a/luci-theme-footstrap/styles/pages/20-overview.css b/luci-theme-footstrap/styles/pages/20-overview.css index ba3fb17e6..e3eca7138 100644 --- a/luci-theme-footstrap/styles/pages/20-overview.css +++ b/luci-theme-footstrap/styles/pages/20-overview.css @@ -341,7 +341,9 @@ /* overflow-wrap dropped — base/40-tables.css breaks every cell; the copy that stood here * moved nothing against 267 matching cells. */ padding-inline-end: min(230px, 50%); - font-size: var(--fs-type); font-weight: var(--fs-weight); color: var(--fs-text); + /* colour and weight are the shared label rule at the end of this file; the size is here + * because it is this row's geometry, not its typography */ + font-size: var(--fs-type); } /* On a narrow card the value takes its own line — a reserve cannot be made to fit there. @@ -371,11 +373,86 @@ /* Drop the per-row dividers theme/30-tables.css adds (.cbi-section .table .td) — * cleaner card. No !important: that rule is `theme`, this file is `page`, so the later - * layer already wins. */ - body[data-page="admin-status-overview"] :is(.fs-ovl-mem, .fs-ovl-sto) .table .td, - body[data-page="admin-status-overview"] :is(.fs-ovl-mem, .fs-ovl-sto) .table .th, - body[data-page="admin-status-overview"] :is(.fs-ovl-mem, .fs-ovl-sto) .cbi-value { border-bottom: 0; } - /* System key/value table draws its dividers on .tr (not .td) — remove those too */ - body[data-page="admin-status-overview"] :is(.fs-ovl-mem, .fs-ovl-sto) .table .tr { border-bottom: 0; } + * layer already wins. + * + * Keyed on the table's SHAPE rather than on the three cards the grid happens to name: a + * key/value table is one with no header row, which is the same test the meter rows above + * already use. Naming `.fs-ovl-mem/-sto` left System ruled at 1px per row — nine dividers down + * a card whose two columns are a name and its value, where the alignment already says which is + * which — and gave a third-party include of the same shape nothing at all. A DATA table + * (`.tr.table-titles` present: DHCP leases, the wifi station list) keeps its dividers: there + * the row is one record among many and the rule is what separates the records. */ + body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .td, + body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .th, + body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr { border-bottom: 0; } + /* Its own rule, not a fourth line in the list above: `.cbi-value` carries no `:has()`, and a + * selector list is not forgiving — below the browser floor the whole list would be discarded, + * dividers and all (npm run css-floor, .claude/rules/css.md). */ + body[data-page="admin-status-overview"] .cbi-section .cbi-value { border-bottom: 0; } + + /* The name in a key/value row is the QUIET half — the figure beside it is what the card is + * read for. It came out at --fs-weight (600) in --fs-text, i.e. heavier and brighter than the + * value it introduces, which is the ranking inverted: four bold lines down a Memory card + * competing with the four numbers they label. --fs-dim at normal weight is what the System + * table's first column already resolved to, so the meter cards now read as the same family. + * Data tables are excluded by the same header-row test as the dividers above: there the first + * column is a record's name, not a caption. */ + body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .td:first-child { + font-weight: var(--fs-weight-normal); + color: var(--fs-dim); + } + + /* ---- the active interface box ---- + * + * theme/45-misc.css fills `.ifacebox-head.active` with --fs-accent and inks it --fs-on-accent. + * On a form that is one small badge; on the Overview it is a full-bleed bar of saturated colour + * across the card — three of them at once here (IPv4 Upstream, radio0, radio1), each louder + * than the data underneath and than the page's own controls. The state it encodes is "this + * interface is up", which is a STATUS, so it is said in the status colour and in the text + * rather than in a fill: --fs-good on the same --fs-panel2 every other head carries. + * + * Scoped to this page deliberately: the same head on Network -> Interfaces sits among many + * boxes where the fill is doing real work telling them apart. */ + body[data-page="admin-status-overview"] .cbi-section .ifacebox .ifacebox-head.active { + background: var(--fs-panel2); + color: var(--fs-good); + font-weight: var(--fs-weight); + } + + /* `L.itemlist` wraps every caption in , which theme/30-tables.css leaves at 700 — so + * inside an interface box the captions (Protocol, Address, Gateway, DNS) are the boldest text + * on the card and the addresses they introduce are not. Same inversion as the key/value rule + * above, same answer; the value keeps --fs-text and needs no rule of its own. */ + body[data-page="admin-status-overview"] .cbi-section .ifacebox-body strong { + font-weight: var(--fs-weight-normal); + color: var(--fs-dim); + } + + /* The meter's own figure, promoted to match the label it now outranks. + * + * theme/25-progressbar.css prints `attr(title)` above the bar at --fs-type-xs in --fs-dim, + * which was the right pairing while the label beside it was 600 in --fs-text. With the label + * moved down (above), a dim 11px figure left the row with nothing at full strength: on Memory + * that is four bars whose only readable text is the caption. The number is what the card is + * for, so it takes the body size and the body ink; the bar and the placement are untouched. + * + * Overview only. The same meter on Software and in the package manager sits in a form row + * where the value is beside its own field label, not carrying the row on its own. */ + body[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .cbi-progressbar::after { + font-size: var(--fs-type); + color: var(--fs-text); + } + + /* Row height in the Overview's DATA tables (DHCP leases, the wifi station list). + * + * theme/30-tables.css pads a data cell for a page that IS a table, where the row is the unit of + * work and the padding is its hit target. On the Overview the same table is a card's contents, + * read at a glance and never clicked except on its buttons, and eight leases at that height are + * most of a screen: the DHCP card measures 740px against a System card of 440. One step down + * the space ladder, so it follows the density axis like everything else, and only here. */ + body[data-page="admin-status-overview"] .cbi-section .table:has(.tr.table-titles) .td, + body[data-page="admin-status-overview"] .cbi-section .table:has(.tr.table-titles) .th { + padding-block: var(--fs-space-1-5); + } } diff --git a/luci-theme-footstrap/styles/theme/20-shell.css b/luci-theme-footstrap/styles/theme/20-shell.css index 8069e5530..0991e0405 100644 --- a/luci-theme-footstrap/styles/theme/20-shell.css +++ b/luci-theme-footstrap/styles/theme/20-shell.css @@ -682,6 +682,27 @@ #fs-nav-progress[data-state="done"] { transition: none; transform: scaleX(1); } } + /* ---- and the swap itself, when the browser can animate it ---- + * The commit is one synchronous DOM move (fs-router.js, swapIn), so this animates a frame rather + * than a render. Same-document view transitions are Baseline since 2025-10-14 (Chrome 111, + * Safari 18, Firefox 144); below that the property parses to nothing and the swap stays instant. + * + * NO `view-transition-name` anywhere, and that is the design: naming `#view` would put two + * elements under one name while the stage is still in the document (fs-router.js stages a second + * `#view`), and a duplicate name aborts the transition; a named element taller than the snapshot + * containing block is also clipped in its own snapshot, which every long status page is. The root + * snapshot is viewport-sized, and bar, rail and sidebar are identical in both halves — what + * visibly cross-fades is the content column. + * + * 140ms against the UA's 250ms: the warm navigation this sits on measures 136-142ms median + * (docs/spa-router.md), and a transition longer than the work it covers reads as lag. + * + * `pointer-events`: the `::view-transition` pseudo covers the viewport and would take every click + * for the length of the animation. Reduced motion is handled in fs-router.js, not here — the `*` + * rule in 95-a11y-media.css cannot reach a pseudo tree. */ + ::view-transition { pointer-events: none; } + ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 140ms; } + /* ---------- footer ---------- * Bare `footer`, not `footer.fs-footer`: the theme owns footer paint outright (absorbed * from base, which keeps only the flex skeleton), so a third-party view emitting its own diff --git a/luci-theme-footstrap/ucode/template/themes/footstrap/header.ut b/luci-theme-footstrap/ucode/template/themes/footstrap/header.ut index b259a4c54..2ea4bac29 100644 --- a/luci-theme-footstrap/ucode/template/themes/footstrap/header.ut +++ b/luci-theme-footstrap/ucode/template/themes/footstrap/header.ut @@ -69,10 +69,22 @@ it, so the CI gate cannot see the difference. */ fs_defaults.layout = fsd.layout || config.main?.footstrap_layout || ''; + /* Optional companion packages announce themselves in the theme's own config — + `uci add_list footstrap.settings.plugin=` from the package's uci-defaults — rather + than being named in this tree. The chrome requires each name and knows nothing else about it, + so the edge points inwards exactly as fs-router.onNavigate does. + + Whitelisted to the shape of a LuCI module name before it is printed into a