mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-31 14:52:06 +00:00
Add actions speed dial
This commit is contained in:
30
resources/web/dialog/SpeedDial/index.html
Normal file
30
resources/web/dialog/SpeedDial/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Speed Dial</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../css/theme.css" />
|
||||
<script type="text/javascript" src="../../include/globalapi.js"></script>
|
||||
<script src="./speeddial.js"></script>
|
||||
</head>
|
||||
<body onload="OnInit()">
|
||||
<div class="launcher">
|
||||
<div class="row-eyebrow fav-eyebrow" id="favEyebrow" hidden></div>
|
||||
<div class="fav-bar" id="favBar"></div>
|
||||
<div class="dial-head">
|
||||
<div class="plugin-search">
|
||||
<span class="plugin-search-icon" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
|
||||
</span>
|
||||
<input class="plugin-search-input" id="q" type="text" placeholder="Search actions" spellcheck="false" autocomplete="off" aria-label="Search actions" />
|
||||
<button class="plugin-search-clear" id="clear" type="button" title="Clear" aria-label="Clear search" hidden>
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="5" x2="11" y2="11"/><line x1="11" y1="5" x2="5" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dial-list" id="list"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
395
resources/web/dialog/SpeedDial/speeddial.js
Normal file
395
resources/web/dialog/SpeedDial/speeddial.js
Normal file
@@ -0,0 +1,395 @@
|
||||
// Speed Dial launcher page. Static-safe module: no DOM access at load time so a
|
||||
// node vm can exercise the pure helpers (filterActions / parseId / nextSel).
|
||||
|
||||
// ---- state (populated by the C++ bridge via window.HandleStudio) ----
|
||||
var ACTIONS = []; // [{id,title,pkg,runnable,shortcut}], already frecency-sorted by C++
|
||||
var FAVS = []; // [id...]
|
||||
var query = "";
|
||||
var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
|
||||
var lastResizeHeight = 0;
|
||||
var matchIndex = {};
|
||||
|
||||
function FoldChar(ch) {
|
||||
return ch.normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
||||
}
|
||||
|
||||
function NormChar(ch) {
|
||||
return FoldChar(ch).toLowerCase();
|
||||
}
|
||||
|
||||
// element handles, assigned in OnInit (kept null so load-time touches no DOM)
|
||||
var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null;
|
||||
|
||||
// ---- pure helpers (no DOM; unit-tested) -------------------------------------
|
||||
function fuzzyRanges(text, query) {
|
||||
var t = text || "";
|
||||
var q = query || "";
|
||||
if (!q)
|
||||
return null;
|
||||
var needle = Array.from(q).map(function (c) { return NormChar(c); }).join("");
|
||||
var ranges = [];
|
||||
var qi = 0;
|
||||
for (var i = 0; i < t.length && qi < needle.length; i++) {
|
||||
if (NormChar(t[i]) === needle[qi]) {
|
||||
if (ranges.length && ranges[ranges.length - 1][1] === i)
|
||||
ranges[ranges.length - 1][1] = i + 1;
|
||||
else
|
||||
ranges.push([i, i + 1]);
|
||||
qi++;
|
||||
}
|
||||
}
|
||||
return qi === needle.length ? ranges : null;
|
||||
}
|
||||
|
||||
function filterActions(actions, query) {
|
||||
var q = (query || "").trim();
|
||||
var list = actions || [];
|
||||
matchIndex = {};
|
||||
if (!q)
|
||||
return list.slice(0);
|
||||
|
||||
var out = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var titleMatch = fuzzyRanges(a.title, q);
|
||||
var pkgMatch = fuzzyRanges(a.pkg, q);
|
||||
if (!titleMatch && !pkgMatch)
|
||||
continue;
|
||||
matchIndex[a.id] = { title: titleMatch, pkg: pkgMatch, useTitle: !!titleMatch };
|
||||
out.push(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// why: ids are "script:<key>::<cap>"; C++ keys off <key> for dont-ask scoping.
|
||||
function parseId(id) {
|
||||
var body = id.slice(id.indexOf(":") + 1); // strip "script:"
|
||||
var sep = body.indexOf("::");
|
||||
return { key: body.slice(0, sep), cap: body.slice(sep + 2) };
|
||||
}
|
||||
|
||||
function visibleFavourites(favourites, actions) {
|
||||
var seen = {};
|
||||
(actions || []).forEach(function (a) { seen[a.id] = true; });
|
||||
return (favourites || []).filter(function (id, i, arr) {
|
||||
return seen[id] && arr.indexOf(id) === i;
|
||||
});
|
||||
}
|
||||
|
||||
function syncClearButton() {
|
||||
if (clearEl)
|
||||
clearEl.hidden = !query;
|
||||
}
|
||||
|
||||
function stateFromPayload(payload) {
|
||||
return {
|
||||
actions: payload.actions || [],
|
||||
favourites: payload.favourites || [],
|
||||
query: "",
|
||||
sel: { zone: "list", i: 0 },
|
||||
lastResizeHeight: 0
|
||||
};
|
||||
}
|
||||
|
||||
function resetScrollPositions(list, doc) {
|
||||
if (list)
|
||||
list.scrollTop = 0;
|
||||
if (doc && doc.scrollingElement)
|
||||
doc.scrollingElement.scrollTop = 0;
|
||||
if (doc && doc.documentElement)
|
||||
doc.documentElement.scrollTop = 0;
|
||||
if (doc && doc.body)
|
||||
doc.body.scrollTop = 0;
|
||||
}
|
||||
|
||||
// nextSel: pure arrow-nav transition. Down fav->list0; Down list->clamp; Up list@0->fav0;
|
||||
// Up list->i-1; Left/Right clamp within fav. Returns a fresh {zone,i}.
|
||||
function nextSel(sel, key, listLen, favLen) {
|
||||
var zone = sel.zone, i = sel.i;
|
||||
if (key === "ArrowDown") {
|
||||
if (zone === "fav") return { zone: "list", i: 0 };
|
||||
return { zone: "list", i: Math.min(i + 1, Math.max(0, listLen - 1)) };
|
||||
}
|
||||
if (key === "ArrowUp") {
|
||||
if (zone === "list") {
|
||||
if (i <= 0) return favLen ? { zone: "fav", i: 0 } : { zone: "list", i: 0 };
|
||||
return { zone: "list", i: i - 1 };
|
||||
}
|
||||
return { zone: zone, i: i };
|
||||
}
|
||||
if (key === "ArrowLeft" && zone === "fav") return { zone: "fav", i: Math.max(0, i - 1) };
|
||||
if (key === "ArrowRight" && zone === "fav") return { zone: "fav", i: Math.min(favLen - 1, i + 1) };
|
||||
return { zone: zone, i: i };
|
||||
}
|
||||
|
||||
// ---- bridge ------------------------------------------------------------------
|
||||
function SendMessage(msg) {
|
||||
if (typeof SendWXMessage !== "function")
|
||||
return;
|
||||
if (typeof msg === "string") msg = { command: msg };
|
||||
if (msg.sequence_id === undefined) msg.sequence_id = Date.now();
|
||||
SendWXMessage(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
// C++ pushes payloads here. Only list_actions is handled; it (re)seeds all state.
|
||||
window.HandleStudio = function (payload) {
|
||||
if (!payload) return;
|
||||
if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (e) { return; } }
|
||||
if (payload.command === "list_actions") {
|
||||
var next = stateFromPayload(payload);
|
||||
ACTIONS = next.actions;
|
||||
FAVS = next.favourites;
|
||||
query = next.query;
|
||||
sel = next.sel;
|
||||
lastResizeHeight = next.lastResizeHeight;
|
||||
if (qEl) {
|
||||
qEl.value = "";
|
||||
syncClearButton();
|
||||
}
|
||||
render({ resize: true, resetScroll: true });
|
||||
focusInput();
|
||||
}
|
||||
};
|
||||
|
||||
// ---- DOM helpers -------------------------------------------------------------
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function byId(id) {
|
||||
for (var i = 0; i < ACTIONS.length; i++) if (ACTIONS[i].id === id) return ACTIONS[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
function currentVisibleFavs() { return visibleFavourites(FAVS, ACTIONS); }
|
||||
|
||||
function hue(id) {
|
||||
var h = 0;
|
||||
for (var i = 0; i < id.length; i++)
|
||||
h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
||||
return h % 360;
|
||||
}
|
||||
|
||||
// Build a <div class=className> with the search-match ranges wrapped in <mark>. Used for both the
|
||||
// title and the pkg eyebrow. Pure (only touches the document factory), so the node-vm test never
|
||||
// calls it and load-time stays DOM-free.
|
||||
function markedText(className, text, match) {
|
||||
var node = document.createElement("div");
|
||||
node.className = className; node.title = text;
|
||||
if (!match || !match.length) {
|
||||
node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
var last = 0;
|
||||
for (var i = 0; i < match.length; i++) {
|
||||
var range = match[i];
|
||||
if (range[0] > last)
|
||||
node.appendChild(document.createTextNode(text.slice(last, range[0])));
|
||||
var m = document.createElement("mark");
|
||||
m.textContent = text.slice(range[0], range[1]);
|
||||
node.appendChild(m);
|
||||
last = range[1];
|
||||
}
|
||||
if (last < text.length)
|
||||
node.appendChild(document.createTextNode(text.slice(last)));
|
||||
return node;
|
||||
}
|
||||
|
||||
function starSvg(on) {
|
||||
return '<svg width="15" height="15" viewBox="0 0 24 24" fill="' + (on ? "currentColor" : "none") +
|
||||
'" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">' +
|
||||
'<path d="M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 17.9 6.8 20.6l1-5.8L3.5 9.7l5.9-.9z"/></svg>';
|
||||
}
|
||||
|
||||
// ---- render ------------------------------------------------------------------
|
||||
function renderFav() {
|
||||
favEl.innerHTML = "";
|
||||
var favs = currentVisibleFavs();
|
||||
favEl.hidden = favs.length === 0;
|
||||
if (!favs.length && sel.zone === "fav")
|
||||
sel = { zone: "list", i: 0 };
|
||||
else if (sel.zone === "fav")
|
||||
sel.i = Math.max(0, Math.min(sel.i, favs.length - 1));
|
||||
updateFavEyebrow(favs);
|
||||
favs.forEach(function (id, i) {
|
||||
var a = byId(id);
|
||||
var tile = document.createElement("button");
|
||||
tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
|
||||
tile.style.setProperty("--h", hue(id));
|
||||
tile.textContent = a.title.charAt(0).toUpperCase();
|
||||
tile.title = a.title;
|
||||
tile.onclick = function () { sel = { zone: "fav", i: i }; run(a); };
|
||||
favEl.appendChild(tile);
|
||||
});
|
||||
}
|
||||
|
||||
// Name of the selected favourite, shown above the bar; hidden unless a fav is selected.
|
||||
function updateFavEyebrow(favs) {
|
||||
if (!eyeEl) return;
|
||||
var a = sel.zone === "fav" && favs.length ? byId(favs[sel.i]) : null;
|
||||
eyeEl.textContent = a ? a.title : "";
|
||||
eyeEl.hidden = !a;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
var q = (query || "").trim();
|
||||
var arr = filterActions(ACTIONS, query);
|
||||
if (sel.zone === "list")
|
||||
sel.i = Math.max(0, Math.min(sel.i, arr.length - 1));
|
||||
listEl.innerHTML = "";
|
||||
// why: search-first - the list stays blank until the user types; only a
|
||||
// non-empty query with zero hits earns the "No actions match" message.
|
||||
if (!q) {
|
||||
listEl.className = "dial-list empty";
|
||||
return;
|
||||
}
|
||||
listEl.className = "dial-list" + (!arr.length ? " empty" : "");
|
||||
if (!arr.length) {
|
||||
var empty = document.createElement("div");
|
||||
empty.className = "dial-empty";
|
||||
empty.textContent = "No actions match";
|
||||
listEl.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
arr.forEach(function (a, i) {
|
||||
var on = FAVS.indexOf(a.id) !== -1;
|
||||
var row = document.createElement("div");
|
||||
row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : "") + (a.runnable === false ? " disabled" : "");
|
||||
|
||||
var tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
tile.style.setProperty("--h", hue(a.id));
|
||||
tile.textContent = a.title.charAt(0).toUpperCase();
|
||||
|
||||
var left = document.createElement("div");
|
||||
left.className = "row-left";
|
||||
var mi = matchIndex[a.id];
|
||||
var pkg = markedText("row-eyebrow", a.pkg, mi ? mi.pkg : null);
|
||||
var line = document.createElement("div");
|
||||
line.className = "row-line";
|
||||
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
||||
line.appendChild(name);
|
||||
if (a.shortcut) {
|
||||
var sc = document.createElement("div");
|
||||
sc.className = "row-sc";
|
||||
a.shortcut.split("+").forEach(function (k) {
|
||||
var key = document.createElement("kbd");
|
||||
key.textContent = k;
|
||||
sc.appendChild(key);
|
||||
});
|
||||
line.appendChild(sc);
|
||||
}
|
||||
left.appendChild(pkg);
|
||||
left.appendChild(line);
|
||||
row.appendChild(tile);
|
||||
row.appendChild(left);
|
||||
|
||||
if (a.runnable !== false) {
|
||||
var star = document.createElement("button");
|
||||
star.className = "star" + (on ? " on" : "");
|
||||
star.innerHTML = starSvg(on);
|
||||
star.title = on ? "Unpin from favourites" : "Pin to favourites";
|
||||
star.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); };
|
||||
// why: two quick fav/unfav clicks must not dblclick-run the row
|
||||
star.ondblclick = function (ev) { ev.stopPropagation(); };
|
||||
row.appendChild(star);
|
||||
}
|
||||
|
||||
row.onclick = function () { sel = { zone: "list", i: i }; render(); };
|
||||
row.ondblclick = function () { sel = { zone: "list", i: i }; run(a); };
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function render(opts) {
|
||||
renderFav();
|
||||
renderList();
|
||||
scrollSelectedIntoView();
|
||||
if (opts && opts.resetScroll)
|
||||
resetScrollPositions(listEl, document);
|
||||
if (opts && opts.resize)
|
||||
requestResize();
|
||||
}
|
||||
|
||||
// Keep the selected item in view as arrows move it: the list scrolls vertically, the fav bar
|
||||
// horizontally (arrow nav "pushes" the scrollable fav row to follow the selection).
|
||||
function scrollSelectedIntoView() {
|
||||
var el = null;
|
||||
if (sel.zone === "list" && listEl)
|
||||
el = listEl.querySelector(".row.sel");
|
||||
else if (sel.zone === "fav" && favEl)
|
||||
el = favEl.querySelector(".fav-tile.sel");
|
||||
if (el && el.scrollIntoView)
|
||||
el.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
}
|
||||
|
||||
function requestResize() {
|
||||
if (!document.body)
|
||||
return;
|
||||
setTimeout(function () {
|
||||
var launcher = document.querySelector(".launcher");
|
||||
if (!launcher)
|
||||
return;
|
||||
var height = Math.ceil(launcher.getBoundingClientRect().height);
|
||||
if (!height || height === lastResizeHeight)
|
||||
return;
|
||||
lastResizeHeight = height;
|
||||
SendMessage({ command: "resize", height: height });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ---- actions -----------------------------------------------------------------
|
||||
function toggleFav(id) {
|
||||
var k = FAVS.indexOf(id);
|
||||
var newState = k === -1;
|
||||
if (newState) FAVS.push(id); else FAVS.splice(k, 1);
|
||||
SendMessage({ command: "toggle_favourite", id: id, fav: newState });
|
||||
render({ resize: true });
|
||||
}
|
||||
|
||||
// Fire the action; C++ owns the run-confirm (native dialog) + suppression, then closes the popup + toasts.
|
||||
function run(a) {
|
||||
if (!a || a.runnable === false) return;
|
||||
SendMessage({ command: "run_action", id: a.id, title: a.title });
|
||||
}
|
||||
|
||||
function runSelected() {
|
||||
if (sel.zone === "fav") { var id = currentVisibleFavs()[sel.i]; if (id) run(byId(id)); return; }
|
||||
var a = filterActions(ACTIONS, query)[sel.i];
|
||||
if (a) run(a);
|
||||
}
|
||||
|
||||
function focusInput() { setTimeout(function () { qEl.focus(); }, 0); }
|
||||
|
||||
// ---- init --------------------------------------------------------------------
|
||||
function OnInit() {
|
||||
qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow");
|
||||
syncClearButton();
|
||||
|
||||
$("clear").onclick = function () {
|
||||
query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; render({ resize: true, resetScroll: true }); qEl.focus();
|
||||
syncClearButton();
|
||||
};
|
||||
qEl.addEventListener("input", function () {
|
||||
query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true });
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
var arr = filterActions(ACTIONS, query);
|
||||
var favs = currentVisibleFavs();
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
sel = nextSel(sel, e.key, arr.length, favs.length);
|
||||
// why: entering/leaving the fav zone toggles the eyebrow line, changing launcher height;
|
||||
// resize so the popup grows/shrinks instead of clipping. requestResize no-ops when unchanged.
|
||||
render({ resize: true });
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
runSelected();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
if (query) { query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); }
|
||||
else SendMessage({ command: "close_page" });
|
||||
}
|
||||
});
|
||||
|
||||
SendMessage({ command: "request_actions" });
|
||||
}
|
||||
185
resources/web/dialog/SpeedDial/style.css
Normal file
185
resources/web/dialog/SpeedDial/style.css
Normal file
@@ -0,0 +1,185 @@
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; }
|
||||
body {
|
||||
font-family: var(--orca-font, "Segoe UI", sans-serif);
|
||||
font-size: 13px;
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
background: var(--bg, var(--orca-bg, #fff));
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
.launcher {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
overflow: hidden;
|
||||
/* why: no height cap here - the launcher reports its true natural height to C++, which
|
||||
sizes the popup to match (HTML is source of truth). The list's own max-height is what
|
||||
bounds growth; capping the launcher would make the measurement circular. */
|
||||
}
|
||||
.fav-bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
border-bottom: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
overflow-x: auto; /* scroll horizontally once favourites overflow the row */
|
||||
scrollbar-width: thin;
|
||||
/* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge,
|
||||
which would clip its selected outline. Matches the 10px horizontal padding. */
|
||||
scroll-padding-inline: 10px;
|
||||
}
|
||||
.fav-bar[hidden] { display: none; }
|
||||
/* Name of the selected favourite, above the bar; left-aligned to the tiles' 10px inset. */
|
||||
.fav-eyebrow { flex: 0 0 auto; padding: 8px 10px 0; }
|
||||
.fav-tile {
|
||||
flex: 0 0 auto; /* keep tiles full-size; don't shrink to fit - scroll instead */
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
|
||||
font-weight: 700;
|
||||
background: var(--speed-tile-bg, #f0f0f0);
|
||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||
}
|
||||
.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
|
||||
.dial-head { flex: 0 0 auto; padding: 8px; }
|
||||
.plugin-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 7px;
|
||||
}
|
||||
.plugin-search:focus-within {
|
||||
border-color: var(--main-color, var(--orca-accent, #009688));
|
||||
box-shadow: 0 0 0 2px rgba(0,150,136,.25);
|
||||
}
|
||||
.plugin-search-icon { display: inline-flex; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.plugin-search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
}
|
||||
.plugin-search-clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
cursor: pointer;
|
||||
background: rgba(127,127,127,.20);
|
||||
}
|
||||
.plugin-search-clear[hidden] { display: none; }
|
||||
.dial-list {
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
/* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so
|
||||
this list cap drives the whole window height: --rows full rows + a ~30% peek of the next row
|
||||
as a "scroll for more" affordance. Bump --rows to show more rows; change 0.3 for a bigger/
|
||||
smaller peek. --row-h MUST match .row min-height (44px). */
|
||||
--row-h: 44px;
|
||||
--rows: 5;
|
||||
max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px);
|
||||
overflow-y: auto;
|
||||
padding: 4px 4px 6px;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.dial-list.empty { overflow-y: hidden; }
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
|
||||
.row.sel { background: var(--row-selected, rgba(0,150,136,.18)); }
|
||||
.tile {
|
||||
flex: 0 0 auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--speed-tile-bg, #f0f0f0);
|
||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||
}
|
||||
.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
||||
.row-eyebrow {
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
opacity: .85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.row-line { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
||||
.row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.row-name mark, .row-eyebrow mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; }
|
||||
.row-sc { flex: 0 0 auto; display: inline-flex; gap: 3px; }
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 11px;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
background: rgba(127,127,127,.12);
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 4px;
|
||||
}
|
||||
.row.disabled { opacity: .5; cursor: not-allowed; }
|
||||
.star {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.star svg { display: block; }
|
||||
.row:hover .star:not(.on),
|
||||
.row.sel .star:not(.on) { opacity: .8; }
|
||||
.star.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.star:hover { background: rgba(127,127,127,.18); }
|
||||
.dial-empty {
|
||||
min-height: 64px;
|
||||
padding: 18px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
text-align: center;
|
||||
}
|
||||
@@ -51,6 +51,12 @@
|
||||
--ctx-bg: #ffffff;
|
||||
--ctx-border: #cccccc;
|
||||
--ctx-hover: #efefef;
|
||||
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #efefef;
|
||||
--speed-tile-border: #d8d8d8;
|
||||
--speed-tile-text-s: 74%;
|
||||
--speed-tile-text-l: 34%;
|
||||
}
|
||||
|
||||
:root[data-orca-theme="dark"] {
|
||||
@@ -84,6 +90,12 @@
|
||||
--ctx-bg: #2a313a;
|
||||
--ctx-border: #4b5664;
|
||||
--ctx-hover: #3a4451;
|
||||
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #34343b;
|
||||
--speed-tile-border: #50505a;
|
||||
--speed-tile-text-s: 82%;
|
||||
--speed-tile-text-l: 84%;
|
||||
}
|
||||
|
||||
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
||||
|
||||
Reference in New Issue
Block a user