fix(#2622): plugin card UX — legible Open button, single badge, visible toggle + reject protocol-relative tab.path
Nathan screenshot feedback on the Plugins card: - Open button rendered as a yellow block with INVISIBLE text: --accent-text resolves to the same gold as --accent in the default theme (text==bg). Switched to a ghost/outline button (accent text + border on the card surface; fills on hover) — always legible regardless of theme. - Removed the redundant DOUBLE 'Enabled' badge (the dashboard-specific badge duplicated the generic activation badge; kept the generic one). - Toggle slider knob was hard to see on the gold 'on' state; added a drop shadow. Also Opus SHOULD-FIX: _VALID_PLUGIN_TAB_PATH now rejects a leading '//' (protocol-relative URL → remote origin in iframe.src). Test updated.
This commit is contained in:
@@ -24,10 +24,11 @@ logger = logging.getLogger(__name__)
|
||||
# a settings key). Lowercase alnum + - / _, 1-64 chars, must start with a letter.
|
||||
_VALID_PLUGIN_NAME = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
|
||||
|
||||
# Valid tab.path: a clean same-origin absolute path. Must start with '/', then
|
||||
# only safe path chars — no quotes, whitespace, control chars, query ('?') or
|
||||
# fragment ('#') so it can't break out of a JS-string nav arg or shadow routes.
|
||||
_VALID_PLUGIN_TAB_PATH = re.compile(r"^/[A-Za-z0-9._~/-]{0,255}$")
|
||||
# Valid tab.path: a clean same-origin absolute path. Must start with a single
|
||||
# '/' (NOT '//' — a leading '//' is a protocol-relative URL that would resolve
|
||||
# to a remote origin when assigned to iframe.src), then only safe path chars —
|
||||
# no quotes, whitespace, control chars, query ('?') or fragment ('#').
|
||||
_VALID_PLUGIN_TAB_PATH = re.compile(r"^/(?!/)[A-Za-z0-9._~/-]{0,255}$")
|
||||
|
||||
# plugin_name -> manifest dict (as loaded from manifest.json)
|
||||
PLUGIN_MANIFESTS: dict[str, dict] = {}
|
||||
|
||||
@@ -6513,7 +6513,6 @@ const enabled=plugin&&plugin.enabled!==false;
|
||||
<div class="provider-card-name">${esc((plugin&&plugin.name)||t('plugins_unnamed'))}</div>
|
||||
<div class="provider-card-meta">${esc((plugin&&plugin.key)||'plugin')}${version}</div>
|
||||
</div>
|
||||
${isDashboardPlugin?`<span class="provider-card-badge ${enabled?'':'plugin-card-badge-disabled'}">${enabled?t('plugins_enabled'):t('plugins_disabled')}</span>`:''}
|
||||
<span class="provider-card-badge ${badgeClass}">${badgeText}</span>
|
||||
</div>
|
||||
<div class="provider-card-body plugin-card-body">
|
||||
|
||||
@@ -3492,11 +3492,15 @@ main.main > #mainPlugin{display:none;}
|
||||
.plugin-toggle-switch{position:relative;display:inline-block;width:32px;height:18px;flex-shrink:0;}
|
||||
.plugin-toggle-switch input{opacity:0;width:0;height:0;}
|
||||
.plugin-toggle-slider{position:absolute;cursor:pointer;inset:0;background:var(--border2);border-radius:9px;transition:background .2s;}
|
||||
.plugin-toggle-slider::before{content:'';position:absolute;height:12px;width:12px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s;}
|
||||
.plugin-toggle-slider::before{content:'';position:absolute;height:12px;width:12px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 2px rgba(0,0,0,.45);}
|
||||
.plugin-toggle-switch input:checked+.plugin-toggle-slider{background:var(--accent);}
|
||||
.plugin-toggle-switch input:checked+.plugin-toggle-slider::before{transform:translateX(14px);}
|
||||
.plugin-open-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;background:var(--accent);color:var(--accent-text);border:none;border-radius:7px;font-size:13px;font-weight:500;cursor:pointer;text-decoration:none;}
|
||||
.plugin-open-btn:hover{opacity:.85;}
|
||||
/* Ghost/outline button: --accent-text is not guaranteed to contrast against
|
||||
--accent in every theme (e.g. it equals --accent in the default gold theme,
|
||||
which renders invisible text on a filled block). Use the accent as the text +
|
||||
border colour on the card's own surface, which is always legible. */
|
||||
.plugin-open-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:transparent;color:var(--accent);border:1px solid var(--accent);border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;text-decoration:none;}
|
||||
.plugin-open-btn:hover{background:var(--accent);color:var(--bg);}
|
||||
|
||||
/* ── Provider model tags ── */
|
||||
.provider-card-models{
|
||||
|
||||
@@ -428,6 +428,7 @@ class TestPluginNameValidation:
|
||||
assert not plugins._VALID_PLUGIN_TAB_PATH.match("/x?y=1") # query
|
||||
assert not plugins._VALID_PLUGIN_TAB_PATH.match("/x#frag") # fragment
|
||||
assert not plugins._VALID_PLUGIN_TAB_PATH.match("/x y") # whitespace
|
||||
assert not plugins._VALID_PLUGIN_TAB_PATH.match("//evil.example/p") # protocol-relative
|
||||
|
||||
def test_open_button_and_toggle_use_no_inline_handlers(self):
|
||||
# tab.path / plugin.key must not be interpolated into inline onclick/
|
||||
|
||||
Reference in New Issue
Block a user