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:
Fusion
2026-04-25 08:25:11 -07:00
committed by gsxdsm
parent 2ae7091049
commit bc0ef85f50
5 changed files with 74 additions and 61 deletions

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}); });