mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Back to spacebar for speed dial shortcut. Integrated inline process settings editing. Optimized saerch results
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -65,32 +65,33 @@ assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
|
||||
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
|
||||
"a leading-space tab title still matches a typed query");
|
||||
|
||||
// settingCode: a setting tile uses the leaf option-name initial (label's last " : "-segment),
|
||||
// escalated by category, then a stable ordinal - so settings aren't all a generic "S".
|
||||
const settings = [
|
||||
{ opt_key: "layer_height", label: "Quality : Layer Height", category: "Quality", type: 0 },
|
||||
{ opt_key: "smooth", label: "Quality : Smooth", category: "Quality", type: 0 }
|
||||
// The main phase is ONE pool: commands/plugins/settings are all actions, ranked by relevance
|
||||
// (no group headers, no actions-vs-settings discrimination).
|
||||
const pool = [
|
||||
{ id: "c1", title: "Layer Height", source: "Quality", group: "Quality : Layers", input: "" },
|
||||
{ id: "s1", title: "Go to layer (percent)", source: "OrcaSlicer", group: "Commands", input: "percent" },
|
||||
{ id: "c2", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", input: "" }
|
||||
];
|
||||
assert.equal(ctx.leafLabel({ label: "Quality : Layer Height", opt_key: "x" }), "Layer Height",
|
||||
"leafLabel takes the last segment of the label");
|
||||
assert.equal(ctx.settingCode(settings[0], settings), "L",
|
||||
"a unique leaf initial yields a single letter");
|
||||
// renderSettingRow iterates settingsResults, so the coded item is always a member of the list.
|
||||
const infillQ = { opt_key: "a", label: "Quality : Infill", category: "Quality", type: 0 };
|
||||
const infillS = { opt_key: "b", label: "Supports : Infill", category: "Supports", type: 0 };
|
||||
const sameLeaf = [infillQ, infillS];
|
||||
assert.equal(ctx.settingCode(infillQ, sameLeaf), "QI",
|
||||
"a colliding leaf initial escalates to category + initial");
|
||||
assert.equal(ctx.settingCode(infillS, sameLeaf), "SI",
|
||||
"a different category disambiguates the same leaf initial");
|
||||
const dupQ = [
|
||||
{ opt_key: "a", label: "Quality : Infill", category: "Quality", type: 0 },
|
||||
{ opt_key: "b", label: "Quality : Infill", category: "Quality", type: 0 }
|
||||
assert.deepEqual(ctx.searchActions(pool, ""), pool, "an empty query returns the pool unchanged");
|
||||
assert.equal(ctx.searchActions(pool, "zzz").length, 0, "a query with no match returns nothing");
|
||||
// "layer" matches multiple; the exact-titled action ranks above the loosely-matching command.
|
||||
assert.equal(ctx.searchActions(pool, "layer")[0].id, "c1",
|
||||
"a title-exact match ranks above a partial match");
|
||||
assert.equal(ctx.searchActions(pool, "layer").length >= 2, true,
|
||||
"both a setting and a command match the same query in the same list");
|
||||
assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2",
|
||||
"a later-but-precise match still ranks by relevance, not by pool type");
|
||||
|
||||
// commandList (the main-phase list) delegates to the ranked search for a typed query and returns
|
||||
// the mixed recents (no discrimination) for an empty query.
|
||||
const mixed = [
|
||||
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" },
|
||||
{ id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" }
|
||||
];
|
||||
assert.equal(ctx.settingCode(dupQ[0], dupQ), "QI1",
|
||||
"both title and category collide -> stable ordinal by opt_key");
|
||||
assert.equal(ctx.settingCode(dupQ[1], dupQ), "QI2",
|
||||
"the ordinal advances by opt_key order");
|
||||
assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd",
|
||||
"a typed query keeps the relevance-ranked action list (best match first)");
|
||||
assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd"],
|
||||
"an empty query shows the mixed recents list verbatim");
|
||||
|
||||
// selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise).
|
||||
assert.equal(
|
||||
@@ -114,4 +115,117 @@ assert.equal(
|
||||
"favourites stay runnable with an empty query - the fav bar is always visible"
|
||||
);
|
||||
|
||||
// Fav quick-launch slots: digit 1..9 -> index 0..8, digit 0 -> index 9 (the 10th), else -1.
|
||||
assert.equal(ctx.favIndexForDigit("1"), 0, "digit 1 maps to the 1st favourite slot");
|
||||
assert.equal(ctx.favIndexForDigit("9"), 8, "digit 9 maps to the 9th favourite slot");
|
||||
assert.equal(ctx.favIndexForDigit("0"), 9, "digit 0 maps to the 10th (last) favourite slot");
|
||||
assert.equal(ctx.favIndexForDigit("x"), -1, "non-digit keys are not a slot");
|
||||
assert.equal(ctx.favIndexForDigit(""), -1, "an empty key is not a slot");
|
||||
|
||||
// Badge label per 0-based index: 0..8 -> "1".."9", index 9 -> "0", out of range -> null.
|
||||
assert.equal(ctx.favSlotForIndex(0), "1", "index 0 shows badge 1");
|
||||
assert.equal(ctx.favSlotForIndex(8), "9", "index 8 shows badge 9");
|
||||
assert.equal(ctx.favSlotForIndex(9), "0", "index 9 shows badge 0 (the 10th slot)");
|
||||
assert.equal(ctx.favSlotForIndex(10), null, "index 10 is beyond the cap");
|
||||
assert.equal(ctx.favSlotForIndex(-1), null, "negative index is not a slot");
|
||||
assert.equal(ctx.K_FAV_LIMIT, 10, "the slot count matches the quick-launch cap");
|
||||
|
||||
// nextSel: arrow-nav wrapping. Down wraps at the list bottom to the first row; Up wraps at the
|
||||
// list top to the last row ONLY when there's no fav bar above (else it goes to the fav bar).
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowDown", 3, 0), { zone: "list", i: 0 },
|
||||
"ArrowDown at the last row wraps to the first row");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 1 }, "ArrowDown", 3, 0), { zone: "list", i: 2 },
|
||||
"ArrowDown in the middle advances by one");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 0), { zone: "list", i: 2 },
|
||||
"ArrowUp at the first row with no fav bar wraps to the last row");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 2), { zone: "fav", i: 0 },
|
||||
"ArrowUp at the first row with a fav bar goes to the fav bar (unchanged)");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowUp", 3, 0), { zone: "list", i: 1 },
|
||||
"ArrowUp in the middle moves up by one");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "fav", i: 1 }, "ArrowDown", 3, 2), { zone: "list", i: 0 },
|
||||
"ArrowDown from the fav bar lands on the first list row");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowDown", 1, 0), { zone: "list", i: 0 },
|
||||
"a single-row list never wraps off the end");
|
||||
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 1, 0), { zone: "list", i: 0 },
|
||||
"ArrowUp on the only row stays put");
|
||||
|
||||
// Windowed list reveal: how many rows must be materialized to cover `fromIndex` plus `size` more,
|
||||
// clamped to the total. Drives the "render the next window on scroll / arrow-nav" append.
|
||||
assert.equal(ctx.revealTarget(100, 0, 100), 100, "covers the whole list when the window reaches the end");
|
||||
assert.equal(ctx.revealTarget(100, 60, 100), 100, "clamps to the total at the tail");
|
||||
assert.equal(ctx.revealTarget(30, 5, 100), 30, "a short list is fully covered");
|
||||
assert.equal(ctx.revealTarget(100, 5, 50), 55, "reveals exactly fromIndex + size");
|
||||
assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to the first row");
|
||||
assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows");
|
||||
assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized");
|
||||
|
||||
// ---- inline setting editor helpers (settingControlRows / Value / CollectedValue) ----
|
||||
const scalarBoolDesc = {
|
||||
editable: true, control: "toggle", cardinality: "scalar", value: true,
|
||||
min: undefined, max: undefined, is_int: false, unit: ""
|
||||
};
|
||||
const scalarRows = ctx.settingControlRows(scalarBoolDesc);
|
||||
assert.equal(scalarRows.length, 1, "a scalar setting yields exactly one control row");
|
||||
assert.equal(scalarRows[0].kind, "toggle", "the control kind is carried through");
|
||||
assert.equal(scalarRows[0].index, 0, "the single row is indexed 0");
|
||||
assert.deepEqual(ctx.settingControlRows({ editable: false }), [], "a non-editable setting yields no control rows");
|
||||
|
||||
const vectorDesc = {
|
||||
editable: true, control: "number", cardinality: "vector", values: [0.2, 0.4],
|
||||
index_labels: ["1", "2"], min: 0, max: 1, is_int: false, unit: "mm"
|
||||
};
|
||||
const vectorRows = ctx.settingControlRows(vectorDesc);
|
||||
assert.equal(vectorRows.length, 2, "a vector setting yields one row per value");
|
||||
assert.deepEqual(vectorRows.map(function (r) { return r.value; }), [0.2, 0.4], "each row carries its current value");
|
||||
assert.deepEqual(vectorRows.map(function (r) { return r.label; }), ["1", "2"], "vector rows use the per-index labels");
|
||||
assert.equal(vectorRows[0].unit, "mm", "the unit is carried to each row");
|
||||
|
||||
// settingControlValue reads a control element (checked/value) as the shipped value.
|
||||
assert.equal(ctx.settingControlValue({ kind: "toggle" }, { checked: true }), true, "a toggle submits its checked state");
|
||||
assert.equal(ctx.settingControlValue({ kind: "toggle" }, { checked: false }), false, "an off toggle submits false");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "0.5" }), 0.5, "a float number parses");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: true, min: 0, max: 10 }, { value: "5" }), 5, "an int number parses");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "2" }), undefined, "an out-of-range number is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "" }), undefined, "an empty number is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "dropdown" }, { dataset: { value: "3" } }), 3, "an enum dropdown submits its stored int value");
|
||||
assert.equal(ctx.settingControlValue({ kind: "dropdown" }, { dataset: { value: "nope" } }), undefined, "a non-numeric dropdown value is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "dropdown" }, {}), undefined, "a dropdown with no value is rejected");
|
||||
// dropdownLabel maps the current int value to its human label, falling back to the value.
|
||||
const seamOptions = [
|
||||
{ value: 0, key: "nearest", label: "Nearest" },
|
||||
{ value: 1, key: "aligned", label: "Aligned" },
|
||||
{ value: 2, key: "random", label: "Random" }
|
||||
];
|
||||
assert.equal(ctx.dropdownLabel(seamOptions, 1), "Aligned", "the dropdown label for a known value is its label");
|
||||
assert.equal(ctx.dropdownLabel(seamOptions, 9), "9", "an unknown value falls back to the raw value");
|
||||
assert.equal(ctx.dropdownLabel([], 3), "3", "empty options fall back to the raw value");
|
||||
assert.equal(ctx.dropdownLabel(
|
||||
[{ value: 1, key: "aligned", label: "" }], 1), "aligned", "a blank label falls back to the key");
|
||||
// dropdownIcon maps the current int value to its pattern pictogram, empty when there is none.
|
||||
const patternOptions = [
|
||||
{ value: 0, key: "rectilinear", label: "Rectilinear" },
|
||||
{ value: 3, key: "gyroid", label: "Gyroid", icon: "data:image/svg+xml;base64,AAA" },
|
||||
{ value: 5, key: "grid", label: "Grid", icon: "data:image/svg+xml;base64,BBB" }
|
||||
];
|
||||
assert.equal(ctx.dropdownIcon(patternOptions, 3), "data:image/svg+xml;base64,AAA", "a known value returns its icon");
|
||||
assert.equal(ctx.dropdownIcon(patternOptions, 0), "", "a value with no icon returns empty");
|
||||
assert.equal(ctx.dropdownIcon(patternOptions, 9), "", "an unknown value returns empty");
|
||||
assert.equal(ctx.dropdownIcon([], 3), "", "empty options return empty");
|
||||
assert.equal(ctx.settingControlValue({ kind: "text" }, { value: "hello" }), "hello", "text submits as a string");
|
||||
// percent (coFloatOrPercent / coFloatsOrPercents): submits the raw typed string; empty is invalid.
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: "10%" }), "10%", "a percent value submits as its string");
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: "0.5" }), "0.5", "an mm value submits as a plain string");
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: " " }), undefined, "a blank percent value is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, {}), undefined, "a percent field with no value is rejected");
|
||||
// A percent scalar-to-scalar payload carries the raw string through unchanged.
|
||||
assert.equal(ctx.settingCollectedValue(
|
||||
{ editable: true, control: "percent", cardinality: "scalar", value: "10%" },
|
||||
[{ kind: "percent", value: "10%" }], [{ value: "10%" }]), "10%", "a percent scalar assembles its string");
|
||||
|
||||
// settingCollectedValue assembles the payload (scalar vs vector) for the set_setting message.
|
||||
assert.equal(ctx.settingCollectedValue({ editable: false }, [], []), undefined, "a non-editable setting yields no payload");
|
||||
assert.equal(ctx.settingCollectedValue(scalarBoolDesc, scalarRows, [{ checked: true }]), true, "a scalar assembles a single value");
|
||||
assert.deepEqual(ctx.settingCollectedValue(vectorDesc, vectorRows, [{ value: "0.3" }, { value: "0.7" }]), [0.3, 0.7], "a vector assembles an array");
|
||||
assert.equal(ctx.settingCollectedValue(vectorDesc, vectorRows, [{ value: "0.3" }, { value: "2" }]), undefined, "an invalid vector element aborts the whole payload");
|
||||
|
||||
console.log("ok");
|
||||
|
||||
@@ -77,6 +77,32 @@ body {
|
||||
.fav-tile:hover .fav-unpin,
|
||||
.fav-tile.sel .fav-unpin { opacity: 1; }
|
||||
.fav-unpin:hover { background: rgba(127,127,127,.6); }
|
||||
/* Numbered quick-launch slot (Alt/Option+digit), top-left corner of the fav tile. */
|
||||
.fav-slot {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
min-width: 11px;
|
||||
height: 11px;
|
||||
padding: 0 2px;
|
||||
border-radius: 3px;
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
line-height: 11px;
|
||||
text-align: center;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
background: rgba(127,127,127,.22);
|
||||
}
|
||||
/* Transient flash banner pinned to the top of the launcher (e.g. "favourites are full"). */
|
||||
.dial-flash {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--plugin-status-warn, #b45309);
|
||||
background: var(--plugin-status-warn-bg, rgba(255,193,7,.22));
|
||||
animation: dial-flash-fade 2.5s ease forwards;
|
||||
}
|
||||
@keyframes dial-flash-fade { 0% { opacity: 0; } 10% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } }
|
||||
.ctx-menu {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
@@ -173,6 +199,9 @@ body {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.dial-list.empty { overflow-y: hidden; }
|
||||
/* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects
|
||||
the full match count, while only a near-viewport window of rows exists in the DOM. */
|
||||
.dial-spacer-bottom { flex: 0 0 auto; }
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -198,6 +227,17 @@ body {
|
||||
background: var(--speed-tile-bg, #f0f0f0);
|
||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||
}
|
||||
/* A tile holding a pattern pictogram: drop the hue fill so the stroked SVG reads cleanly. */
|
||||
.tile.has-icon,
|
||||
.fav-tile.has-icon {
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
.tile-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
}
|
||||
.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
||||
.row-eyebrow {
|
||||
font-size: 10px;
|
||||
@@ -254,3 +294,145 @@ kbd {
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- inline setting editor card (phase "setting") --------------------------- */
|
||||
.dial-list.setting { overflow-y: auto; }
|
||||
.dial-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
.editor-title { font-size: 14px; font-weight: 600; line-height: 1.3; }
|
||||
.editor-title-row { display: flex; align-items: center; gap: 8px; }
|
||||
.editor-preview-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.editor-preview-icon[hidden] { display: none; }
|
||||
.editor-tooltip { font-size: 11px; line-height: 1.4; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.editor-row { display: flex; align-items: center; gap: 8px; min-height: 32px; }
|
||||
.editor-label {
|
||||
flex: 0 0 auto;
|
||||
min-width: 64px;
|
||||
font-size: 12px;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.editor-unit { font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.editor-row input[type="number"],
|
||||
.editor-row input[type="text"],
|
||||
.editor-row select,
|
||||
.editor-row input[type="color"] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
}
|
||||
.editor-row input[type="color"] { padding: 2px 4px; cursor: pointer; }
|
||||
.editor-row input:focus { outline: none; border-color: var(--main-color, var(--orca-accent, #009688)); box-shadow: 0 0 0 2px rgba(0,150,136,.22); }
|
||||
/* custom dropdown (native <select> popups are flaky in the embedded webview) */
|
||||
.ed-dropdown { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
||||
.ed-dropdown-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ed-dropdown-toggle.open { border-color: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.ed-dropdown-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ed-dropdown-caret { flex: 0 0 auto; font-size: 10px; color: var(--muted, var(--orca-muted, #6b7280)); transition: transform .12s; }
|
||||
.ed-dropdown-toggle.open .ed-dropdown-caret { transform: rotate(180deg); }
|
||||
.ed-dropdown-menu {
|
||||
/* Overlay popover, not in-flow: opening it must NOT grow the editor card or the popup window.
|
||||
position/left/top/width/max-height are set from JS on open (position:fixed escapes the
|
||||
scrollable card and launcher overflow, so the menu clips to the popup and scrolls itself).
|
||||
These are only the visual defaults. */
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.16);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ed-dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ed-dropdown-option:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
|
||||
.ed-dropdown-option.sel { font-weight: 600; color: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.ed-option-label { flex: 1 1 auto; min-width: 0; }
|
||||
/* Pattern pictograms: filled with the option's icon, and (for values with none) absent. */
|
||||
.ed-option-icon,
|
||||
.ed-dropdown-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.ed-dropdown-icon[hidden] { display: none; }
|
||||
/* toggle switch */
|
||||
.ed-switch { position: relative; flex: 0 0 auto; width: 36px; height: 20px; }
|
||||
.ed-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
|
||||
.ed-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #c4c4c4;
|
||||
border-radius: 10px;
|
||||
transition: background .15s;
|
||||
}
|
||||
.ed-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform .15s;
|
||||
}
|
||||
.ed-switch input:checked + .ed-slider { background: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.ed-switch input:checked + .ed-slider::before { transform: translateX(16px); }
|
||||
.editor-readonly { padding: 4px 0; font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.editor-actions { display: flex; gap: 8px; padding-top: 4px; }
|
||||
.ed-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ed-btn:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
|
||||
.ed-btn-primary { background: var(--main-color, var(--orca-accent, #009688)); border-color: var(--main-color, var(--orca-accent, #009688)); color: #fff; }
|
||||
.ed-btn-primary:hover { filter: brightness(1.05); }
|
||||
.editor-hint { font-size: 10px; text-align: center; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
|
||||
Reference in New Issue
Block a user