mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 21:55:13 +08:00
🎉 Sync 2026-08-31 01:44:18
This commit is contained in:
1 parent
54f91cd44a
commit
1444a85fae
18 files changed
+308
-34
No files matched your search
@@ -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 */
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user