Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions apps/plugins/parts/installed-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import '../../../src/ui/components/os-card/os-card';
import '../../../src/ui/components/os-chip/os-chip';
import '../../../src/ui/components/os-cluster/os-cluster';
import '../../../src/ui/components/os-grid/os-grid';
import '../../../src/ui/components/os-icon/os-icon';
import '../../../src/ui/components/os-stack/os-stack';
import '../../../src/ui/components/os-tabs/os-tabs';
import { externalLink, fallbackGlyph, htmlBlock, linkButton, stripHtml, wpOrgUrl } from './html';
import { externalLink, htmlBlock, linkButton, stripHtml, wpOrgUrl } from './html';
import { attachIconFallback } from './icon-fallback';
import { buildStarCluster } from './card';
import { emptyState, loadingLine, renderReviews } from './reviews';
Expand Down Expand Up @@ -136,11 +137,11 @@ function buildHero( row: InstalledPlugin ): HTMLElement {
img.loading = 'lazy';
img.decoding = 'async';
img.src = attachIconFallback( img, iconUrl, () => {
iconTile.replaceChildren( fallbackGlyph() );
iconTile.replaceChildren( detailIcon( 'admin-plugins', 20 ) );
} );
iconTile.appendChild( img );
} else {
iconTile.appendChild( fallbackGlyph() );
iconTile.appendChild( detailIcon( 'admin-plugins', 20 ) );
}

const titleBlock = document.createElement( 'os-stack' );
Expand Down Expand Up @@ -410,9 +411,7 @@ function pushFactCard( parent: HTMLElement, icon: string, label: string, value:
const head = document.createElement( 'div' );
head.setAttribute( 'slot', 'header' );
head.className = 'os-plugins__detail-fact-head';
const ico = document.createElement( 'span' );
ico.className = `dashicons dashicons-${ icon }`;
ico.setAttribute( 'aria-hidden', 'true' );
const ico = detailIcon( icon, 14 );
const lab = document.createElement( 'span' );
lab.textContent = label;
head.append( ico, lab );
Expand All @@ -430,14 +429,31 @@ function pushFactCard( parent: HTMLElement, icon: string, label: string, value:

// ─── Small helpers ─────────────────────────────────────────────────

/**
* Create an `<os-icon>` element configured for the inspector.
*
* Bare Dashicons spans rely on the document-level stylesheet which
* cannot penetrate this inspector's shadow DOM boundary. `<os-icon>`
* resolves the glyph codepoint at runtime and emits it as text
* content so it renders reliably inside any shadow root.
*
* @param name Dashicon slug or class name.
* @param size Icon size in pixels. Defaults to 14.
* @return The configured `<os-icon>` element.
*/
function detailIcon( name: string, size = 14 ): HTMLElement {
const ico = document.createElement( 'os-icon' );
ico.setAttribute( 'name', name );
ico.setAttribute( 'size', String( size ) );
return ico;
}

function chip( icon: string, label: string ): HTMLElement {
const c = document.createElement( 'os-chip' );
c.setAttribute( 'label', label );
c.setAttribute( 'tone', 'neutral' );
const ico = document.createElement( 'span' );
const ico = detailIcon( icon, 14 );
ico.setAttribute( 'slot', 'icon' );
ico.className = `dashicons dashicons-${ icon }`;
ico.setAttribute( 'aria-hidden', 'true' );
c.appendChild( ico );
return c;
}
Expand Down
4 changes: 2 additions & 2 deletions apps/plugins/parts/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const PANEL_STYLES = `
.os-plugins__detail-hero-inner { display: flex; align-items: center; gap: 14px; padding: 14px 24px; }
.os-plugins__detail-hero-icon { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 10px; overflow: hidden; background: var( --os-ui-surface, rgba( 0, 0, 0, 0.04 ) ); box-shadow: 0 0 0 1px var( --os-ui-border, rgba( 0, 0, 0, 0.08 ) ) inset; display: flex; align-items: center; justify-content: center; }
.os-plugins__detail-hero-icon img { width: 100%; height: 100%; max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.os-plugins__detail-hero-icon .dashicons { font-size: 20px; width: 20px; height: 20px; line-height: 20px; color: var( --os-ui-fg-muted, #888 ); }
.os-plugins__detail-hero-icon os-icon, .os-plugins__detail-hero-icon .dashicons { font-size: 20px; width: 20px; height: 20px; line-height: 20px; color: var( --os-ui-fg-muted, #888 ); }
.os-plugins__detail-hero-text { flex: 1 1 auto; min-width: 0; }
.os-plugins__detail-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.25; letter-spacing: -0.005em; color: var( --os-ui-fg, inherit ); }
.os-plugins__detail-byline { margin: 0; font-size: 12.5px; color: var( --os-ui-fg-muted, #666 ); }
Expand Down Expand Up @@ -48,7 +48,7 @@ const PANEL_STYLES = `
.os-plugins__detail-grid { width: 100%; }
.os-plugins__detail-fact { min-width: 0; }
.os-plugins__detail-fact-head { display: flex; align-items: center; gap: 8px; color: var( --os-ui-fg-muted, #666 ); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
.os-plugins__detail-fact-head .dashicons { font-size: 14px; width: 14px; height: 14px; line-height: 14px; }
.os-plugins__detail-fact-head os-icon, .os-plugins__detail-fact-head .dashicons { font-size: 14px; width: 14px; height: 14px; line-height: 14px; }
.os-plugins__detail-fact-value { font-size: 14px; color: var( --os-ui-fg, inherit ); word-break: break-word; overflow-wrap: anywhere; font-weight: 500; }
.os-plugins__detail-fact-value code { font-family: var( --os-ui-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace ); font-size: 12.5px; background: var( --os-ui-surface-sunken, rgba( 0, 0, 0, 0.06 ) ); padding: 2px 7px; border-radius: 4px; font-weight: 400; }
.os-plugins__detail-fact-value a { color: var( --wp-admin-theme-color, #2271b1 ); text-decoration: none; }
Expand Down
29 changes: 29 additions & 0 deletions apps/plugins/plugins.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**

Check warning on line 1 in apps/plugins/plugins.test.ts

View workflow job for this annotation

GitHub Actions / Lint · Types · Vitest

This file is 1049 lines — past the 1000-line comfort zone. Be smart, build robust software: modules of ~300–600 lines are easier to read, test, review and reuse. Consider splitting this one along its natural seams — future you will say thanks
* Plugins app — the client half: the view frame (tabs gated by the
* caps, the status filter and its update count, the bulk bar, the
* phone layout), the installed-list filter, the admin-ajax client
Expand Down Expand Up @@ -1016,4 +1016,33 @@
expect( flyout.querySelector( '.os-plugins__flyout-hero-title' )?.textContent ).toBe( 'Save & Go <img src=x onerror=alert(1)>' );
expect( flyout.querySelector( '.os-plugins__flyout-hero-title img' ) ).toBeNull();
} );

it( 'renders fact cards and chips with <os-icon> inside the inspector shadow root', () => {
const { root } = mount( {}, { installed: [ row( { version: '2.10.2', requires_wp: '6.0' } ) ] } );
root.querySelector< HTMLElement >( '[data-plugin-details]' )!.click();
const shadow = root.querySelector( '.os-plugins__inspector-content' )?.shadowRoot;
expect( shadow ).not.toBeNull();

// Check overview chips use <os-icon>
const chipIcon = shadow!.querySelector( 'os-chip os-icon[name="wordpress"]' );
expect( chipIcon ).not.toBeNull();
expect( chipIcon?.getAttribute( 'size' ) ).toBe( '14' );

// Switch to Details tab to inspect fact cards.
const tabs = shadow!.querySelector( 'os-tabs' )!;
tabs.dispatchEvent( new CustomEvent( 'os-tab-change', { detail: { value: 'details' } } ) );

const tagIcon = shadow!.querySelector( '.os-plugins__detail-fact-head os-icon[name="tag"]' );
expect( tagIcon ).not.toBeNull();
expect( tagIcon?.getAttribute( 'size' ) ).toBe( '14' );

const fileIcon = shadow!.querySelector( '.os-plugins__detail-fact-head os-icon[name="media-document"]' );
expect( fileIcon ).not.toBeNull();
expect( fileIcon?.getAttribute( 'size' ) ).toBe( '14' );

// Fallback hero icon when plugin has no icon URL.
const heroIcon = shadow!.querySelector( '.os-plugins__detail-hero-icon os-icon[name="admin-plugins"]' );
expect( heroIcon ).not.toBeNull();
expect( heroIcon?.getAttribute( 'size' ) ).toBe( '20' );
} );
} );
Loading