mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-27 22:34:51 -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";
|
originBadge.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var unlockBtn = document.getElementById("etm-unlock");
|
var lockBtn = document.getElementById("etm-lock-btn");
|
||||||
if (unlockBtn) {
|
if (lockBtn) {
|
||||||
unlockBtn.style.display = isReadonly ? "" : "none";
|
// Inline-flex (not "") so display: none doesn't bleed into a
|
||||||
unlockBtn.dataset.skillId = tmplId;
|
// browser-default block reflow on re-show.
|
||||||
unlockBtn.dataset.skillName = tmpl.name || "";
|
lockBtn.style.display = isReadonly ? "inline-flex" : "none";
|
||||||
|
lockBtn.dataset.skillId = tmplId;
|
||||||
|
lockBtn.dataset.skillName = tmpl.name || "";
|
||||||
}
|
}
|
||||||
var submitBtn = document.getElementById("etm-submit");
|
var submitBtn = document.getElementById("etm-submit");
|
||||||
if (submitBtn) {
|
if (submitBtn) {
|
||||||
submitBtn.style.display = "";
|
submitBtn.style.display = "";
|
||||||
submitBtn.textContent = isReadonly ? "Save Config" : "Save";
|
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).
|
// Spec/content fields: locked for installed skills (preserve source fidelity).
|
||||||
// Point screen readers at the origin badge so the "why is this disabled?"
|
// Point screen readers at the origin badge so the "why is this disabled?"
|
||||||
@@ -1538,7 +1545,7 @@ function hideEditTemplateModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unlockSkill() {
|
function unlockSkill() {
|
||||||
var btn = document.getElementById("etm-unlock");
|
var btn = document.getElementById("etm-lock-btn");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
var skillId = btn.dataset.skillId;
|
var skillId = btn.dataset.skillId;
|
||||||
var skillName = btn.dataset.skillName || "this skill";
|
var skillName = btn.dataset.skillName || "this skill";
|
||||||
@@ -1557,11 +1564,8 @@ function unlockSkill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _performUnlockSkill(skillId) {
|
function _performUnlockSkill(skillId) {
|
||||||
var btn = document.getElementById("etm-unlock");
|
var btn = document.getElementById("etm-lock-btn");
|
||||||
if (btn) {
|
if (btn) btn.disabled = true;
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = "Customizing…";
|
|
||||||
}
|
|
||||||
authFetch("/v1/api/admin/skills/" + skillId + "/unlock", {
|
authFetch("/v1/api/admin/skills/" + skillId + "/unlock", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
@@ -1585,10 +1589,9 @@ function _performUnlockSkill(skillId) {
|
|||||||
showToast(e.message || "Unlock failed");
|
showToast(e.message || "Unlock failed");
|
||||||
})
|
})
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
if (btn) {
|
// Re-enable in case the re-render didn't happen (error path); the
|
||||||
btn.disabled = false;
|
// success path hides the button anyway via showEditTemplateModal.
|
||||||
btn.textContent = "Customize…";
|
if (btn) btn.disabled = false;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3298,6 +3298,17 @@
|
|||||||
class="admin-modal admin-modal-wide admin-modal-skill"
|
class="admin-modal admin-modal-wide admin-modal-skill"
|
||||||
>
|
>
|
||||||
<h2 id="edit-template-title">Edit Skill</h2>
|
<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
|
<div
|
||||||
id="etm-origin-badge"
|
id="etm-origin-badge"
|
||||||
class="skill-origin-badge"
|
class="skill-origin-badge"
|
||||||
@@ -3579,15 +3590,6 @@
|
|||||||
<button class="modal-cancel" onclick="hideEditTemplateModal()">
|
<button class="modal-cancel" onclick="hideEditTemplateModal()">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</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
|
<button
|
||||||
id="etm-submit"
|
id="etm-submit"
|
||||||
class="modal-submit"
|
class="modal-submit"
|
||||||
|
|||||||
@@ -1855,6 +1855,11 @@
|
|||||||
.admin-modal-skill {
|
.admin-modal-skill {
|
||||||
padding: 28px 28px 24px;
|
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 {
|
.skill-spec-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -2085,37 +2090,47 @@ textarea.skill-content-area {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
.modal-secondary {
|
/* Lock-icon affordance in the top-right of an installed (readonly) skill's
|
||||||
/* Sized to content so the primary action (Save) keeps a stable footprint
|
edit modal — clicking detaches the skill from upstream so spec fields
|
||||||
whether or not Customize is rendered. The spacer below pushes Save right. */
|
become editable. Reserved for the modal it lives in; not a generic
|
||||||
padding: 9px 14px;
|
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;
|
background: transparent;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font: inherit;
|
font-size: 14px;
|
||||||
font-family: var(--font-ui);
|
/* Render the lock glyph as text where supported (keeps the monochrome
|
||||||
font-size: 12px;
|
instrument-panel aesthetic instead of a coloured emoji). Browsers
|
||||||
font-weight: 500;
|
that don't support font-variant-emoji fall back gracefully. */
|
||||||
|
font-variant-emoji: text;
|
||||||
cursor: pointer;
|
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);
|
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: 2px solid var(--accent);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
.modal-secondary:disabled {
|
.skill-lock-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.modal-buttons-spacer {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#create-user-overlay,
|
#create-user-overlay,
|
||||||
#create-token-overlay,
|
#create-token-overlay,
|
||||||
@@ -2155,6 +2170,14 @@ textarea.skill-content-area {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 600;
|
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 display (show-once) */
|
||||||
.token-created-warning {
|
.token-created-warning {
|
||||||
@@ -4008,7 +4031,7 @@ textarea.skill-content-area {
|
|||||||
.admin-action-btn,
|
.admin-action-btn,
|
||||||
.modal-cancel,
|
.modal-cancel,
|
||||||
.modal-submit,
|
.modal-submit,
|
||||||
.modal-secondary {
|
.skill-lock-btn {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
.admin-modal input,
|
.admin-modal input,
|
||||||
|
|||||||
Reference in New Issue
Block a user