mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-13 23:42:25 -06:00
fix(skills): unlock UX — lock icon top-right, save reset, confirm z-index
Three issues from manual smoke-testing the unlock flow:
1. Confirm dialog rendered behind the edit-skill modal. Both
overlays sat at z-index 600, and confirm-overlay is earlier in
the DOM than edit-template-overlay — so DOM order put the parent
modal on top of its own confirm. Bumped confirm-overlay to 650
(still below toasts at 700) since confirm dialogs are launched
FROM other overlays and need to sit above them.
2. Save button stayed disabled (or non-functional) after unlock.
submitEditTemplate disables etm-submit on click and re-enables in
.finally, but a stale disabled=true survives the mutate-in-place
re-render that runs after unlock. Always reset
submitBtn.disabled = false in showEditTemplateModal so the
re-render path can never inherit a stuck disabled state.
3. UX redesign — moved the unlock affordance from a "Customize…"
button at the bottom of the footer to a 🔒 icon button at the
top-right of the modal. The lock glyph is the universal "this is
locked, click to unlock" affordance and reads more clearly than
a footer button next to Cancel/Save. font-variant-emoji: text
keeps it monochrome on browsers that support it (instrument-panel
aesthetic) with graceful fallback to coloured emoji elsewhere.
admin-modal-skill h2 reserves padding-right so a long title can
never collide with the absolute-positioned button.
Cleanup: removed the now-unused .modal-secondary and
.modal-buttons-spacer rules; the bottom etm-unlock button + flex
spacer are gone from the modal footer.
This commit is contained in:
@@ -1446,16 +1446,23 @@ function showEditTemplateModal(tmplId) {
|
||||
originBadge.style.display = "none";
|
||||
}
|
||||
}
|
||||
var unlockBtn = document.getElementById("etm-unlock");
|
||||
if (unlockBtn) {
|
||||
unlockBtn.style.display = isReadonly ? "" : "none";
|
||||
unlockBtn.dataset.skillId = tmplId;
|
||||
unlockBtn.dataset.skillName = tmpl.name || "";
|
||||
var lockBtn = document.getElementById("etm-lock-btn");
|
||||
if (lockBtn) {
|
||||
// Inline-flex (not "") so display: none doesn't bleed into a
|
||||
// browser-default block reflow on re-show.
|
||||
lockBtn.style.display = isReadonly ? "inline-flex" : "none";
|
||||
lockBtn.dataset.skillId = tmplId;
|
||||
lockBtn.dataset.skillName = tmpl.name || "";
|
||||
}
|
||||
var submitBtn = document.getElementById("etm-submit");
|
||||
if (submitBtn) {
|
||||
submitBtn.style.display = "";
|
||||
submitBtn.textContent = isReadonly ? "Save Config" : "Save";
|
||||
// Always reset to enabled — submitEditTemplate disables this on click
|
||||
// and re-enables in .finally, but a stale disabled=true survives a
|
||||
// mutate-in-place re-render (e.g. after unlock) and would leave the
|
||||
// button non-functional otherwise.
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
// Spec/content fields: locked for installed skills (preserve source fidelity).
|
||||
// Point screen readers at the origin badge so the "why is this disabled?"
|
||||
@@ -1538,7 +1545,7 @@ function hideEditTemplateModal() {
|
||||
}
|
||||
|
||||
function unlockSkill() {
|
||||
var btn = document.getElementById("etm-unlock");
|
||||
var btn = document.getElementById("etm-lock-btn");
|
||||
if (!btn) return;
|
||||
var skillId = btn.dataset.skillId;
|
||||
var skillName = btn.dataset.skillName || "this skill";
|
||||
@@ -1557,11 +1564,8 @@ function unlockSkill() {
|
||||
}
|
||||
|
||||
function _performUnlockSkill(skillId) {
|
||||
var btn = document.getElementById("etm-unlock");
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Customizing…";
|
||||
}
|
||||
var btn = document.getElementById("etm-lock-btn");
|
||||
if (btn) btn.disabled = true;
|
||||
authFetch("/v1/api/admin/skills/" + skillId + "/unlock", {
|
||||
method: "POST",
|
||||
})
|
||||
@@ -1585,10 +1589,9 @@ function _performUnlockSkill(skillId) {
|
||||
showToast(e.message || "Unlock failed");
|
||||
})
|
||||
.finally(function () {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Customize…";
|
||||
}
|
||||
// Re-enable in case the re-render didn't happen (error path); the
|
||||
// success path hides the button anyway via showEditTemplateModal.
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3298,6 +3298,17 @@
|
||||
class="admin-modal admin-modal-wide admin-modal-skill"
|
||||
>
|
||||
<h2 id="edit-template-title">Edit Skill</h2>
|
||||
<button
|
||||
id="etm-lock-btn"
|
||||
class="skill-lock-btn"
|
||||
type="button"
|
||||
style="display: none"
|
||||
onclick="unlockSkill()"
|
||||
aria-label="Customize skill (detach from upstream and unlock editing)"
|
||||
title="Customize — detach from upstream and unlock editing"
|
||||
>
|
||||
<span aria-hidden="true">🔒</span>
|
||||
</button>
|
||||
<div
|
||||
id="etm-origin-badge"
|
||||
class="skill-origin-badge"
|
||||
@@ -3579,15 +3590,6 @@
|
||||
<button class="modal-cancel" onclick="hideEditTemplateModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
id="etm-unlock"
|
||||
class="modal-secondary"
|
||||
style="display: none"
|
||||
onclick="unlockSkill()"
|
||||
>
|
||||
Customize…
|
||||
</button>
|
||||
<span class="modal-buttons-spacer" aria-hidden="true"></span>
|
||||
<button
|
||||
id="etm-submit"
|
||||
class="modal-submit"
|
||||
|
||||
@@ -1855,6 +1855,11 @@
|
||||
.admin-modal-skill {
|
||||
padding: 28px 28px 24px;
|
||||
}
|
||||
/* Reserve space for the absolute-positioned lock button (top-right) so a
|
||||
long title can never collide with it. */
|
||||
.admin-modal-skill > h2 {
|
||||
padding-right: 44px;
|
||||
}
|
||||
|
||||
.skill-spec-body {
|
||||
display: grid;
|
||||
@@ -2085,37 +2090,47 @@ textarea.skill-content-area {
|
||||
cursor: not-allowed;
|
||||
filter: none;
|
||||
}
|
||||
.modal-secondary {
|
||||
/* Sized to content so the primary action (Save) keeps a stable footprint
|
||||
whether or not Customize is rendered. The spacer below pushes Save right. */
|
||||
padding: 9px 14px;
|
||||
/* Lock-icon affordance in the top-right of an installed (readonly) skill's
|
||||
edit modal — clicking detaches the skill from upstream so spec fields
|
||||
become editable. Reserved for the modal it lives in; not a generic
|
||||
button class. */
|
||||
.skill-lock-btn {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
font: inherit;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
/* Render the lock glyph as text where supported (keeps the monochrome
|
||||
instrument-panel aesthetic instead of a coloured emoji). Browsers
|
||||
that don't support font-variant-emoji fall back gracefully. */
|
||||
font-variant-emoji: text;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
z-index: 2;
|
||||
}
|
||||
.modal-secondary:hover {
|
||||
.skill-lock-btn:hover {
|
||||
background: var(--bg-highlight);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.modal-secondary:focus-visible {
|
||||
.skill-lock-btn:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.modal-secondary:disabled {
|
||||
.skill-lock-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
.modal-buttons-spacer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#create-user-overlay,
|
||||
#create-token-overlay,
|
||||
@@ -2155,6 +2170,14 @@ textarea.skill-content-area {
|
||||
justify-content: center;
|
||||
z-index: 600;
|
||||
}
|
||||
/* Confirm dialogs are launched FROM other overlays (e.g. unlock-skill from
|
||||
the edit-template modal). They share z-index 600, so DOM order picks the
|
||||
winner — and confirm-overlay is earlier in the DOM, so it would render
|
||||
underneath. Bump it above the per-feature overlays but keep it below
|
||||
toasts (z-index 700). */
|
||||
#confirm-overlay {
|
||||
z-index: 650;
|
||||
}
|
||||
|
||||
/* Token display (show-once) */
|
||||
.token-created-warning {
|
||||
@@ -4008,7 +4031,7 @@ textarea.skill-content-area {
|
||||
.admin-action-btn,
|
||||
.modal-cancel,
|
||||
.modal-submit,
|
||||
.modal-secondary {
|
||||
.skill-lock-btn {
|
||||
transition: none;
|
||||
}
|
||||
.admin-modal input,
|
||||
|
||||
Reference in New Issue
Block a user