fix(FN-2509): polish plugin and pi extension UX
- Apply shared button utility classes across PluginManager action controls for consistent dashboard styling - Move PluginManager mobile responsive overrides into PluginManager.css and update code background fallback to token-based color-mix - Improve PiExtensionsManager remove controls with touch-target sizing, hover feedback, and descriptive aria-labels - Stabilize ModelOnboardingModal helper-text assertions by waiting for provider fetch/render state before checking visibility
This commit is contained in:
@@ -247,16 +247,20 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: var(--space-xs);
|
padding: var(--space-xs);
|
||||||
|
min-width: var(--space-2xl);
|
||||||
|
min-height: var(--space-2xl);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
transition: color var(--transition-fast);
|
transition: color var(--transition-fast), background-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pi-ext-remove-btn:hover {
|
.pi-ext-remove-btn:hover {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
|
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pi-ext-remove-btn:focus-visible {
|
.pi-ext-remove-btn:focus-visible {
|
||||||
@@ -369,17 +373,21 @@
|
|||||||
.pi-ext-resource-remove {
|
.pi-ext-resource-remove {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: var(--space-xs);
|
||||||
|
min-width: var(--space-2xl);
|
||||||
|
min-height: var(--space-2xl);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
transition: color var(--transition-fast);
|
transition: color var(--transition-fast), background-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pi-ext-resource-remove:hover {
|
.pi-ext-resource-remove:hover {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
|
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pi-ext-resource-remove:focus-visible {
|
.pi-ext-resource-remove:focus-visible {
|
||||||
@@ -389,48 +397,6 @@
|
|||||||
|
|
||||||
/* Mobile responsive overrides */
|
/* Mobile responsive overrides */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Detail view header */
|
|
||||||
.plugin-manager-detail-header {
|
|
||||||
gap: var(--space-sm);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-detail-title {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Detail cards */
|
|
||||||
.plugin-detail-card {
|
|
||||||
padding: var(--space-md);
|
|
||||||
gap: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plugin list items */
|
|
||||||
.plugin-list {
|
|
||||||
gap: var(--space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-item {
|
|
||||||
padding: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action buttons inside list items */
|
|
||||||
.plugin-actions {
|
|
||||||
gap: var(--space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Detail actions footer — buttons stack on narrow screens */
|
|
||||||
.plugin-detail-actions {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-detail-actions button {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pi Extensions list items - mobile adjustments */
|
/* Pi Extensions list items - mobile adjustments */
|
||||||
.pi-ext-list {
|
.pi-ext-list {
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
|
|||||||
@@ -219,9 +219,10 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
|||||||
<span key={index} className="pi-ext-resource-tag">
|
<span key={index} className="pi-ext-resource-tag">
|
||||||
<span className="pi-ext-resource-path">{path}</span>
|
<span className="pi-ext-resource-path">{path}</span>
|
||||||
<button
|
<button
|
||||||
className="pi-ext-resource-remove"
|
className="btn-icon touch-target pi-ext-resource-remove"
|
||||||
onClick={() => handleRemoveResource(type, path)}
|
onClick={() => handleRemoveResource(type, path)}
|
||||||
title={`Remove ${path}`}
|
title={`Remove ${path}`}
|
||||||
|
aria-label={`Remove ${path}`}
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={12} />
|
||||||
</button>
|
</button>
|
||||||
@@ -331,9 +332,10 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className="pi-ext-remove-btn"
|
className="btn-icon touch-target pi-ext-remove-btn"
|
||||||
onClick={() => handleRemovePackage(source)}
|
onClick={() => handleRemovePackage(source)}
|
||||||
title="Remove package"
|
title="Remove package"
|
||||||
|
aria-label={`Remove package ${label}`}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -41,7 +41,10 @@
|
|||||||
.plugin-install-hint code {
|
.plugin-install-hint code {
|
||||||
padding: 1px var(--space-xs);
|
padding: 1px var(--space-xs);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--bg-tertiary, rgba(127, 127, 127, 0.12));
|
background: var(
|
||||||
|
--bg-tertiary,
|
||||||
|
color-mix(in srgb, var(--text-muted) 18%, transparent)
|
||||||
|
);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,3 +224,40 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.plugin-manager-detail-header {
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-detail-title {
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-detail-card {
|
||||||
|
padding: var(--space-md);
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-list {
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-item {
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-actions {
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-detail-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-detail-actions button {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
className="btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const current = (pluginSettings[key] as unknown[]) || [];
|
const current = (pluginSettings[key] as unknown[]) || [];
|
||||||
const defaultItem = schema.itemType === "number" ? 0 : "";
|
const defaultItem = schema.itemType === "number" ? 0 : "";
|
||||||
@@ -410,7 +410,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button className="btn-primary" onClick={handleSaveSettings}>
|
<button className="btn btn-primary" onClick={handleSaveSettings}>
|
||||||
Save Settings
|
Save Settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,7 +422,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
<div className="plugin-detail-actions">
|
<div className="plugin-detail-actions">
|
||||||
{selectedPlugin.state === "started" && (
|
{selectedPlugin.state === "started" && (
|
||||||
<button
|
<button
|
||||||
className="btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => handleReload(selectedPlugin)}
|
onClick={() => handleReload(selectedPlugin)}
|
||||||
disabled={reloadingPluginId === selectedPlugin.id}
|
disabled={reloadingPluginId === selectedPlugin.id}
|
||||||
>
|
>
|
||||||
@@ -431,11 +431,11 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{selectedPlugin.enabled ? (
|
{selectedPlugin.enabled ? (
|
||||||
<button className="btn-secondary" onClick={() => handleDisable(selectedPlugin)}>
|
<button className="btn btn-secondary" onClick={() => handleDisable(selectedPlugin)}>
|
||||||
Disable
|
Disable
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button className="btn-primary" onClick={() => handleEnable(selectedPlugin)}>
|
<button className="btn btn-primary" onClick={() => handleEnable(selectedPlugin)}>
|
||||||
Enable
|
Enable
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -456,7 +456,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
<button className="btn-icon" onClick={loadPlugins} title="Refresh">
|
<button className="btn-icon" onClick={loadPlugins} title="Refresh">
|
||||||
<RefreshCw size={16} className={loading ? "spin" : ""} />
|
<RefreshCw size={16} className={loading ? "spin" : ""} />
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-primary" onClick={() => setShowInstall(true)}>
|
<button className="btn btn-primary" onClick={() => setShowInstall(true)}>
|
||||||
<Plus size={14} /> Install
|
<Plus size={14} /> Install
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -479,10 +479,10 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="plugin-install-actions">
|
<div className="plugin-install-actions">
|
||||||
<button className="btn-primary" onClick={handleInstall} disabled={installing || !installPath.trim()}>
|
<button className="btn btn-primary" onClick={handleInstall} disabled={installing || !installPath.trim()}>
|
||||||
{installing ? "Installing..." : "Install Plugin"}
|
{installing ? "Installing..." : "Install Plugin"}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-secondary" onClick={() => { setShowInstall(false); setInstallPath(""); }}>
|
<button className="btn btn-secondary" onClick={() => { setShowInstall(false); setInstallPath(""); }}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2881,8 +2881,10 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper text should be visible when no providers are authenticated
|
// Wait for provider fetch to settle, then confirm helper visibility.
|
||||||
expect(screen.getByText("Skip this step if you'd like — you can always add providers later from Settings.")).toBeTruthy();
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Skip this step if you'd like — you can always add providers later from Settings.")).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show helper text on AI Setup when a provider is authenticated", async () => {
|
it("does not show helper text on AI Setup when a provider is authenticated", async () => {
|
||||||
@@ -2900,7 +2902,10 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper text should NOT be visible when a provider is authenticated
|
// Wait for provider sections to render (auth fetch complete), then assert helper absence.
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Quick start providers")).toBeTruthy();
|
||||||
|
});
|
||||||
expect(screen.queryByText("Skip this step if you'd like — you can always add providers later from Settings.")).toBeNull();
|
expect(screen.queryByText("Skip this step if you'd like — you can always add providers later from Settings.")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user