diff --git a/.claude/skills/pf-align/SKILL.md b/.claude/skills/pf-align/SKILL.md new file mode 100644 index 000000000..5ee07aa20 --- /dev/null +++ b/.claude/skills/pf-align/SKILL.md @@ -0,0 +1,175 @@ +--- +name: pf-align +description: >- + This skill should be used when aligning a PatternFly Java component with its + PatternFly counterpart by implementing action items from pf-compare reports. + Triggers include /pf-align, "align component", "implement missing variations", + "fix PFJ component", "apply pf-compare recommendations", "add missing PFJ + variations", "sync with PatternFly", "bring component up to date", + "implement pf-compare action items", "implement comparison findings", + "fix alignment issues", or any request to implement changes identified + by a comparison report. +metadata: + version: "0.2.0" +--- + +# /pf-align — PatternFly Java Alignment + +Implements action items from `/pf-compare` reports. Simple fixes (CSS modifiers, attributes) are auto-generated. Complex items (new variations, structural changes, icons) are presented with context for the user to implement manually. + +## Arguments + +``` +/pf-align [--item ] +``` + +- **component** (required) — Component name matching the report file (e.g., `button`, `card`). **Reject `template`** — it is a blueprint component used as a reference for project conventions, not a real UI component. +- **--item** (optional) — Process only the specified action item number (default: process all) + +--- + +## Step 1: Pre-flight Checks + +1. **Parse arguments**: Extract component name and optional item filter. + +2. **Check report exists**: `reports/pf-compare/.json` + - If not found: "Report not found. Run `/pf-compare ` first." + +3. **Locate component files**: + - Component class: `components/src/main/java/org/patternfly/component//.java` + - Showcase file: `showcase/src/main/java/org/patternfly/showcase/component/Component.java` + - For compound names (e.g., `description-list`), convert to PascalCase: `DescriptionList.java` + - If not found, search: `find components/src -name ".java" -path "*/component/*"` + +4. **Print**: "Pre-flight OK. Report: `reports/pf-compare/.json`" + +--- + +## Step 2: Parse & Present Action Items + +1. **Read the JSON report** at `reports/pf-compare/.json`. Extract: + - `pfUrl` — PF React showcase URL + - `variations` — array of `{ slug, title, html }` with raw PF HTML + - `actionItems` — array of `{ number, type, priority, title, description, category, variations }` + +2. **Classify items** into two categories: + + | Category | Types | What happens | + |----------|-------|-------------| + | **Simple** (auto-fix) | `fix_css`, `fix_attribute` | Code is generated and applied automatically | + | **Complex** (manual) | `add_variation`, `fix_structure`, `fix_icon`, `implement_feature` | Context is presented, user implements | + +3. **Print action items** grouped by category: + ``` + Action items from reports/pf-compare/button.json: + + Simple (auto-fix): + 3. [P3] Fix attribute: Hamburger aria-expanded — add aria-expanded attribute + + Complex (manual): + 1. [P1] Add variation: Custom component — PF section not in PFJ + 2. [P2] Fix CSS: Circle tertiary — add pf-m-tertiary modifier + 4. [P4] Fix icon: Favorite — different icon set (viewBox mismatch) + ``` + +4. **Handle --item filter**: If `--item N` specified, filter to only that item. + +5. **Ask user**: "Process all items? Or specify which items to work on." + +--- + +## Step 3: Process Items + +For each selected item, read the component class and showcase file to understand existing patterns (modifiers, ARIA methods, section markers, snippet IDs). + +### Simple items (fix_css, fix_attribute) + +Read `references/code-generation.md` for the insertion pattern. + +**fix_css**: +1. Extract modifier name from description (e.g., `.pf-m-danger` → `danger`) +2. Check if modifier method already exists in component class +3. If missing: generate modifier method, insert in `// ------------------------------------------------------ builder` section +4. Show diff, ask for approval + +**fix_attribute**: +1. Extract attribute from description (e.g., `aria-label` → `ariaLabel`) +2. Check if ARIA method already exists in component class +3. If missing: generate ARIA method, insert in `// ------------------------------------------------------ aria` section +4. Show diff, ask for approval + +### Complex items (add_variation, fix_structure, fix_icon, implement_feature) + +Present context to help the user implement manually: + +1. **Show PF reference HTML**: Look up the section title in the `variations` array from the report. Print the raw HTML (or a relevant excerpt if very long). If HTML is not in the report, print the `pfUrl` and section title so the user can inspect it in a browser. + +2. **Show relevant PFJ code**: Print the relevant section of the component class or showcase file. For add_variation: show existing snippet patterns. For fix_structure: show the affected method. For fix_icon: show the icon usage. + +3. **Describe the difference**: Restate the action item description with enough context for the user to act on it. + +4. **Print guidance**: + ``` + This item requires manual implementation: + - PF reference:
(HTML shown above) + - PFJ file: + - What to change: + ``` + +5. **Do not generate code** for complex items. + +--- + +## Step 4: Build Verify + +After all simple fixes are applied: + +1. Run: `mvn verify -Dquickly -P showcase` +2. If build fails: show error, ask user whether to revert changes or keep them and fix manually +3. If build succeeds: continue to summary + +Skip this step if no simple fixes were applied (only complex items presented). + +--- + +## Step 5: Summary + +Print a summary: + +``` +## pf-align: + +Applied: + - [P3] Fix attribute: Hamburger aria-expanded ✓ + +Manual (context presented): + - [P1] Add variation: Custom component + - [P4] Fix icon: Favorite + +Files modified: + - components/src/main/java/.../Button.java (1 method added) + +Next steps: + - Implement the manual items listed above + - Re-run /pf-compare after changes to verify alignment +``` + +--- + +## Error Handling + +| Situation | Action | +|-----------|--------| +| Report not found | Print error, suggest running `/pf-compare` first | +| Component class not found | Print error with search paths tried | +| Item number invalid | Print error, list valid numbers | +| Method already exists | Skip item, print "Already exists: `methodName()`" | +| Build failure | Show error, ask to revert or keep | +| HTML not in report | Print `pfUrl` + section title for manual inspection | + +## Anti-Patterns + +- **Do not generate code for complex items.** Show context and let the user implement. +- **Do not extract HTML from the browser.** Use only what's in the pf-compare report. +- **Do not write an align report.** Progress is tracked through git history. +- **Do not modify showcase files automatically.** Only component class changes (modifier/ARIA methods) are auto-generated. diff --git a/.claude/skills/pf-align/examples/sample-report-input.json b/.claude/skills/pf-align/examples/sample-report-input.json new file mode 100644 index 000000000..5b6922c1c --- /dev/null +++ b/.claude/skills/pf-align/examples/sample-report-input.json @@ -0,0 +1,84 @@ +{ + "component": "badge", + "date": "2026-05-28", + "pfVersion": "Release 6.1", + "pfUrl": "https://www.patternfly.org/components/badge/html", + "pfjUrl": "http://localhost:1234/components/badge", + "completeness": { + "pfTotal": 3, + "pfjTotal": 2, + "matched": 2, + "missingInPfj": ["with-tooltip"], + "extraInPfj": [] + }, + "variations": [ + { "slug": "read", "title": "Read", "html": "
7
" }, + { "slug": "unread", "title": "Unread", "html": "
7
" }, + { "slug": "with-tooltip", "title": "With tooltip", "html": "
7
" } + ], + "snippets": [ + { "id": "read-badges", "title": "Read badges", "html": "
7
" }, + { "id": "unread-badges", "title": "Unread badges", "html": "
7
" } + ], + "matches": [ + { + "pfVariation": { "slug": "read", "title": "Read" }, + "pfjSnippet": { "id": "read-badges", "title": "Read badges" }, + "matchType": "semantic", + "status": "matched" + }, + { + "pfVariation": { "slug": "unread", "title": "Unread" }, + "pfjSnippet": { "id": "unread-badges", "title": "Unread badges" }, + "matchType": "semantic", + "status": "matched" + }, + { + "pfVariation": { "slug": "with-tooltip", "title": "With tooltip" }, + "pfjSnippet": null, + "matchType": "none", + "status": "missing_in_pfj" + } + ], + "comparisons": [ + { + "variation": "Read", + "status": "ok", + "missingClasses": [], + "extraClasses": [], + "structuralDiffs": [], + "attributeDiffs": [], + "iconDiffs": [] + }, + { + "variation": "Unread", + "status": "differences_found", + "missingClasses": [ + { "class": "pf-m-overflow", "element": "", "context": "present in PF, absent in PFJ" } + ], + "extraClasses": [], + "structuralDiffs": [], + "attributeDiffs": [], + "iconDiffs": [] + } + ], + "actionItems": [ + { + "number": 1, + "type": "add_variation", + "priority": "P1", + "title": "With tooltip", + "description": "implement badge with tooltip on hover", + "category": "missing_sections" + }, + { + "number": 2, + "type": "fix_css", + "priority": "P2", + "title": "Unread", + "description": "add .pf-m-overflow modifier class", + "category": "dom_differences", + "variations": ["Unread"] + } + ] +} diff --git a/.claude/skills/pf-align/references/code-generation.md b/.claude/skills/pf-align/references/code-generation.md new file mode 100644 index 000000000..bec174a8f --- /dev/null +++ b/.claude/skills/pf-align/references/code-generation.md @@ -0,0 +1,44 @@ +# Code Generation Reference + +Patterns for auto-generated simple fixes. For complex items (add_variation, fix_structure, fix_icon, implement_feature), the skill presents context only — the user implements manually. + +## fix_css — Add Modifier Method + +1. Extract modifier name from the CSS class (e.g., `.pf-m-danger` → `danger`) +2. Check if the method already exists in the component class +3. Generate: + ```java + public danger() { + return css(modifier("danger")); + } + ``` +4. Insert in `// ------------------------------------------------------ builder` section, after existing modifier methods, alphabetically sorted + +## fix_attribute — Add ARIA Method + +1. Extract attribute name (e.g., `aria-label` → `ariaLabel`, `aria-expanded` → `ariaExpanded`) +2. Check if the method already exists in the component class +3. Generate: + ```java + // String value + public ariaLabel(String label) { + return aria(Aria.label, label); + } + + // Boolean value + public ariaExpanded(boolean expanded) { + return aria(Aria.expanded, expanded); + } + ``` +4. Insert in `// ------------------------------------------------------ aria` section, after existing ARIA methods, alphabetically sorted + +## General Rules + +These apply when generating code and are also useful as reference when implementing complex items manually: + +- **Modifier methods** return `this` via `css(modifier("name"))` +- **ARIA methods** return `this` via `aria(Aria.name, value)` +- **Indentation**: 4 spaces, match existing style +- **Imports**: Add `import static org.patternfly.style.Classes.modifier` if not present +- **Section markers**: `// ------------------------------------------------------
` — respect these boundaries +- **ElementTextDelegate vs ElementTextMethods**: Use Delegate when text is in a child element, Methods when the component IS the text element diff --git a/.claude/skills/pf-compare/SKILL.md b/.claude/skills/pf-compare/SKILL.md new file mode 100644 index 000000000..0eea73165 --- /dev/null +++ b/.claude/skills/pf-compare/SKILL.md @@ -0,0 +1,260 @@ +--- +name: pf-compare +description: >- + This skill should be used when comparing a PatternFly component against its + PatternFly Java (PFJ) implementation. Triggers include /pf-compare, + "compare PF component", "check PFJ completeness", "compare button component", + "what's missing in the Java card", "gap analysis for alert", + "generate comparison report for tabs", "find missing PF variations", + "PF coverage report", "DOM differences", or any request to identify variation + coverage gaps or DOM/CSS differences between PatternFly and PatternFly Java. +metadata: + version: "0.2.0" +--- + +# /pf-compare — PatternFly Component Comparison + +Compares a PatternFly (React) component against its PatternFly Java implementation. Uses two phases: section coverage (which demo sections exist?) and DOM comparison (how do matched sections differ structurally?). + +## Tools + +Uses Chrome DevTools MCP tools for browser interaction (navigate_page, new_page, select_page, evaluate_script, close_page, list_pages, take_snapshot). Requires approval on first use. + +## Arguments + +``` +/pf-compare [--port ] +``` + +- **component** (required) — URL slug of a single component (e.g., `button`, `card`, `data-list`). One component per invocation. **Reject `template`** — it is a blueprint component, not a real UI component. +- **--port** (optional) — PFJ local dev server port (default: `1234`) + +Parse from the ARGUMENTS string. If no component is provided, ask the user which component to compare. + +--- + +## Step 1: Pre-flight Checks + +Run in order. Stop on failure. + +1. **Parse arguments**: Extract `COMPONENT` and optional `--port` (default `1234`). If no component is provided, ask the user. + +2. **Resolve PFJ showcase URL** — try in order: + - Run: `curl -s -o /dev/null -w "%{http_code}" http://localhost:/components/` + - If HTTP 200, set `PFJ_URL = http://localhost:/components/` + - Otherwise, use the published showcase: set `PFJ_URL = https://patternfly-java.github.io/components/` + - Note: The published PFJ showcase is a single-page app with client-side routing. `curl` will return 404 for deep links, but the page loads correctly in a browser. Do NOT use curl to verify this URL — trust it and verify via browser navigation in Step 2. + +3. **Verify PF site**: + - Run: `curl -s -o /dev/null -w "%{http_code}" -L https://www.patternfly.org/components/` + - Note the `-L` flag to follow redirects (PF site uses 302 redirects to add trailing slashes). + - If not 200, stop with: "Could not reach PF showcase at `https://www.patternfly.org/components/`. Verify the component name." + +4. **Set variables**: `PF_URL = https://www.patternfly.org/components/`, `PFJ_URL`, `COMPONENT` + +5. **Print**: "Pre-flight OK. Comparing PF (`PF_URL`) against PFJ (`PFJ_URL`)." + +--- + +## Step 2: Extract Sections + +Extract example sections from both pages using jump links navigation. + +### 2a: PF sections + +1. Open `PF_URL` in a new Chrome tab via `new_page`. +2. Read the script from `references/extract-sections.js` and pass it to `evaluate_script` with `args: ["pf", COMPONENT]`. The component slug is needed to validate that each section has a corresponding `ws-react-c-{component}-{id}` preview container. +3. Store the result as `PF_SECTIONS` — an array of `{ id, title, group }`. +4. Report: "Extracted N PF sections: (list of titles)" + +### 2b: PFJ sections + +1. Open `PFJ_URL` in a new Chrome tab via `new_page`. +2. Read the script from `references/extract-sections.js` and pass it to `evaluate_script` with `args: ["pfj"]`. +3. Store the result as `PFJ_SECTIONS` — an array of `{ id, title, group }`. +4. If `PFJ_SECTIONS` is empty, warn the user that the PFJ page may not have loaded correctly and suggest checking `PFJ_URL` in a browser manually before continuing. +5. Report: "Extracted M PFJ sections: (list of titles)" + +--- + +## Step 3: Match Sections + +Perform AI-assisted semantic matching between `PF_SECTIONS` and `PFJ_SECTIONS`. Apply rules in order: + +1. **Exact match** (case-insensitive title comparison) — automatic match. +2. **Semantic match** (e.g., "Aria-disabled examples" matches "Aria-disabled") — match with a note explaining the reasoning. +3. **No match** — mark as `missing_in_pfj`. + +After matching, check for unmatched PFJ sections — mark as `extra_in_pfj`. + +Produce three lists: + +- **MATCHED** — array of `{ pf: { id, title, group }, pfj: { id, title, group } }` +- **MISSING_IN_PFJ** — PF sections with no PFJ counterpart +- **EXTRA_IN_PFJ** — PFJ sections with no PF counterpart + +Report the matching table showing each PF section, its matched PFJ section (or "---"), the group, and the match status. + +--- + +## Step 4: DOM Comparison + +For each entry in `MATCHED`, compare the rendered preview HTML. + +### 4a: Extract preview HTML + +For each matched pair: + +1. **PF preview**: Switch to the PF tab. Run `evaluate_script` to extract innerHTML from the preview container. The container has `id="ws-react-c--"` (e.g., `ws-react-c-button-variant-examples`). Extract its `innerHTML`. + + ```javascript + (sectionId) => { + const el = document.getElementById(sectionId); + return el ? el.innerHTML : null; + } + ``` + + Pass `args: ["ws-react-c-" + COMPONENT + "-" + pf.id]`. + +2. **PFJ preview**: Switch to the PFJ tab. Run `evaluate_script` to find the `[data-pfj-demo]` element within the section identified by the PFJ heading ID. The heading has `id=""`. Walk up to the parent stack, find the `[data-pfj-demo]` element, and extract its `innerHTML`. + + ```javascript + (headingId) => { + const heading = document.getElementById(headingId); + if (!heading) return null; + const stack = heading.parentElement?.parentElement?.parentElement; + if (!stack) return null; + const demo = stack.querySelector('[data-pfj-demo]'); + return demo ? demo.innerHTML : null; + } + ``` + + Pass `args: [pfj.id]`. + +Store both HTML strings as `PF_HTML` and `PFJ_HTML`. + +### 4b: Normalize both sides + +For each matched pair: + +1. Read the script from `references/normalize-dom.js`. +2. Run it via `evaluate_script` twice — once with `PF_HTML` and once with `PFJ_HTML` as the argument. +3. Store the results as `PF_ELEMENTS` and `PFJ_ELEMENTS` — arrays of element signature objects. + +### 4c: AI comparison + +For each matched section, compare `PF_ELEMENTS` and `PFJ_ELEMENTS`. Classify each difference by priority: + +| Priority | Category | What to look for | +|----------|----------|-----------------| +| **P1** | Missing component elements | PF has an element with a `pf-v6-c-*` class that has no counterpart in PFJ (by class signature). This usually means a sub-component or structural element is missing. | +| **P2** | Modifier differences | Same element exists on both sides (matching `pf-v6-c-*` class) but different `pf-m-*` modifiers. A modifier combination in PF has no match in PFJ, or vice versa. | +| **P3** | Attribute differences | Same element, but different `aria-*` attributes, `role`, `type`, `disabled`, or `tabindex` values. Missing or different `aria-label` is a common finding. | +| **P4** | Icon differences | SVG elements with different `viewBox` values. This usually indicates different icon sets (FontAwesome vs Red Hat icons). | +| **P5** | Cosmetic / showcase-only | Element count differences from showcase layout (flex wrappers, spacing elements), text content differences, or differences in non-functional attributes. | + +For detailed normalization rules and what `normalize-dom.js` strips vs. keeps, see `references/ignore-patterns.md`. + +**Guidelines for the AI comparison:** +- Compare elements by their `classes` array (component class signature). Two elements "match" if they share the same primary `pf-v6-c-*` class. +- Do NOT report differences in element count alone — only report what specifically differs. +- When PF has more elements than PFJ, identify which specific elements are missing, not just the count delta. +- Consider showcase layout context: flex wrappers (`pf-v6-l-flex`, `pf-v6-l-stack`) are often showcase layout, not component structure. +- If both sides have the same component elements with the same modifiers and attributes, report "ok". + +### 4d: Store results + +For each matched section, produce: +```json +{ + "section": "", + "status": "ok | differences_found", + "differences": [ + { + "priority": "P1", + "description": "..." + } + ] +} +``` + +Store as `COMPARISON_RESULTS`. + +--- + +## Step 5: Print Inline Summary + +Print the following summary directly in the conversation: + +``` +## PF Compare: + +### Section Coverage: / sections (%) + +Missing in PFJ: + - title 1 + - title 2 + +Extra in PFJ: + - title 1 + +### DOM Differences: + section: P2 (2 modifier diffs), P3 (1 attribute diff) + section: OK + section: P4 (1 icon diff) + +Full report: reports/pf-compare/.md +``` + +--- + +## Step 6: Write Detailed Report + +1. Create the output directory: + ``` + mkdir -p reports/pf-compare + ``` + +2. Detect the PF version by switching to the PF tab and running the script from `references/detect-pf-version.js` via `evaluate_script`. + +3. Generate the report file using the template from `references/report-template.md`. Read that file and fill in the placeholders with actual data. + +4. Write the report to `reports/pf-compare/.md`. + +5. **Write the JSON companion report** to `reports/pf-compare/.json`. This file contains the same data in structured form. Use the schema from `references/report-schema.json` and match the format in `examples/button.json`. The JSON includes: + - Metadata: skillVersion (from `metadata.version` in this SKILL.md), component, date, pfVersion, pfUrl, pfjUrl + - Section coverage: pfCount, pfjCount, matched, missingInPfj, extraInPfj + - Full `variations` array from Step 4a — each entry has `{ slug, title, html }` where `slug` is the PF section ID, `title` is the section heading, and `html` is the raw (pre-normalization) PF preview innerHTML. This array is consumed by `/pf-align` for reference HTML. + - Action items with number, type, priority, title, description, category, and affected variations + +6. Report: "Reports saved to `reports/pf-compare/.md` and `reports/pf-compare/.json`" + +--- + +## Step 7: Cleanup + +1. Use `list_pages` to find the PF and PFJ tabs opened during the workflow. +2. Close them with `close_page` (keep at least one tab open in the browser). +3. Print completion message with next steps: + - Review the report at `reports/pf-compare/.md` + - JSON data available at `reports/pf-compare/.json` (used by `/pf-align`) + - Use the report as input for a future `/pf-align` skill + - Run `/pf-compare` on another component + +--- + +## Error Handling + +- **Component not found**: If curl returns non-200 for both PFJ URLs, report clearly and suggest checking the component slug or starting the dev server. +- **No sections extracted**: If PF or PFJ returns zero sections, warn the user that the page structure may have changed and the jump links selectors may need updating. +- **Chrome DevTools unavailable**: If MCP tools fail, report the error and suggest ensuring Chrome is running with DevTools MCP connected. +- **Preview HTML not found**: If a preview container is not found for a matched section, skip that section's DOM comparison and note it in the report. This can happen if the PF page uses a different preview container pattern for that section. +- **Partial data**: If only some comparisons succeed, still produce the report with available data and note which comparisons failed. + +## Anti-Patterns + +- **Blocking on cosmetic differences**: Do not report P5 cosmetic differences as blocking issues. +- **Comparing non-PF classes**: Only compare classes prefixed with `pf-v6-` or `pf-m-`. Ignore application-specific or utility classes. +- **Deep SVG comparison**: Do not traverse SVG internals. Only compare the `viewBox` attribute. +- **Overwriting reports without asking**: If a report already exists at the target path, note it in the output but proceed with overwriting (the file is regenerable). +- **Reporting element counts as issues**: An element count difference alone is not a finding. Identify what specifically is missing or extra. diff --git a/.claude/skills/pf-compare/examples/button.json b/.claude/skills/pf-compare/examples/button.json new file mode 100644 index 000000000..34a82e2f0 --- /dev/null +++ b/.claude/skills/pf-compare/examples/button.json @@ -0,0 +1,63 @@ +{ + "skillVersion": "0.2.0", + "component": "button", + "date": "2026-06-08", + "pfVersion": "Release 6.5.1", + "pfUrl": "https://www.patternfly.org/components/button", + "pfjUrl": "http://localhost:1234/components/button", + "sections": { + "pfCount": 17, + "pfjCount": 18, + "matched": 16, + "missingInPfj": ["Custom component"], + "extraInPfj": ["Types"] + }, + "variations": [ + { "slug": "variant-examples", "title": "Variant examples", "html": "
...
" }, + { "slug": "disabled-buttons", "title": "Disabled buttons", "html": "
...
" }, + { "slug": "small-buttons", "title": "Small buttons", "html": "
...
" }, + { "slug": "call-to-action-cta-buttons", "title": "Call to action (CTA) buttons", "html": "
...
" }, + { "slug": "block-level-buttons", "title": "Block level buttons", "html": "
...
" }, + { "slug": "progress-indicators", "title": "Progress indicators", "html": "
...
" }, + { "slug": "links-as-buttons", "title": "Links as buttons", "html": "
...
" }, + { "slug": "inline-link-as-span", "title": "Inline link as span", "html": "
...
" }, + { "slug": "custom-component", "title": "Custom component", "html": "
...
" }, + { "slug": "aria-disabled-examples", "title": "Aria-disabled examples", "html": "
...
" }, + { "slug": "button-with-count", "title": "Button with count", "html": "
...
" }, + { "slug": "plain-with-no-padding", "title": "Plain with no padding", "html": "
...
" }, + { "slug": "stateful", "title": "Stateful", "html": "
...
" }, + { "slug": "circle-buttons", "title": "Circle buttons", "html": "
...
" }, + { "slug": "favorite", "title": "Favorite", "html": "
...
" }, + { "slug": "settings", "title": "Settings", "html": "
...
" }, + { "slug": "hamburger", "title": "Hamburger", "html": "
...
" } + ], + "actionItems": [ + { + "number": 1, + "type": "add_variation", + "priority": "P1", + "title": "Custom component", + "description": "PF has a 'Custom component' section demonstrating button rendered as a custom element; PFJ has no equivalent section", + "category": "missing_sections", + "variations": ["Custom component"] + }, + { + "number": 2, + "type": "fix_attribute", + "priority": "P3", + "title": "Hamburger aria-expanded", + "description": "PF hamburger button has aria-expanded='false', PFJ has none; add aria-expanded attribute toggled on open/close", + "category": "dom_differences", + "variations": ["Hamburger"] + }, + { + "number": 3, + "type": "fix_icon", + "priority": "P4", + "title": "Favorite icon set", + "description": "PFJ uses Red Hat star icon (viewBox 0 0 32 32), PF uses FontAwesome star (viewBox 0 0 576 512); switch to FontAwesome for consistency", + "category": "dom_differences", + "variations": ["Favorite"] + } + ] +} diff --git a/.claude/skills/pf-compare/examples/button.md b/.claude/skills/pf-compare/examples/button.md new file mode 100644 index 000000000..f09bcd01d --- /dev/null +++ b/.claude/skills/pf-compare/examples/button.md @@ -0,0 +1,78 @@ +--- +component: button +date: 2026-06-08 +pf_version: Release 6.5.1 +pf_url: https://www.patternfly.org/components/button +pfj_url: http://localhost:1234/components/button +sections: + pf_count: 17 + pfj_count: 18 + matched: 16 + missing_in_pfj: + - Custom component + extra_in_pfj: + - Types +--- + +# PF Compare: button + +## Section Coverage + +| # | PF Section | PFJ Section | Group | Status | +|---|------------|-------------|-------|--------| +| 1 | Variant examples | Variant examples | Examples | matched | +| 2 | Disabled buttons | Disabled buttons | Examples | matched | +| 3 | Small buttons | Small buttons | Examples | matched | +| 4 | Call to action (CTA) buttons | Call to action (CTA) buttons | Examples | matched | +| 5 | Block level buttons | Block level buttons | Examples | matched | +| 6 | Progress indicators | Progress indicators | Examples | matched | +| 7 | Links as buttons | Links as buttons | Examples | matched | +| 8 | Inline link as span | Inline link as span | Examples | matched | +| 9 | Custom component | --- | Examples | missing_in_pfj | +| 10 | Aria-disabled examples | Aria-disabled | Examples | matched | +| 11 | Button with count | Button with count | Examples | matched | +| 12 | Plain with no padding | Plain with no padding | Examples | matched | +| 13 | Stateful | Stateful | Examples | matched | +| 14 | Circle buttons | Circle buttons | Examples | matched | +| 15 | Favorite | Favorite | Animated examples | matched | +| 16 | Settings | Settings | Animated examples | matched | +| 17 | Hamburger | Hamburger | Animated examples | matched | +| -- | --- | Types | --- | extra_in_pfj | + +## DOM Differences + +### Variant examples + +**Status:** differences_found + +#### P3: Attribute Differences +- Plain button: PF has `aria-label="Remove"`, PFJ has none + +### Disabled buttons + +**Status:** ok + +### Hamburger + +**Status:** differences_found + +#### P3: Attribute Differences +- Hamburger button: PF has `aria-expanded="false"`, PFJ has none +- Hamburger button: PF has `aria-label="Hamburger"`, PFJ has `aria-label="Menu"` + +### Favorite + +**Status:** differences_found + +#### P4: Icon Differences +- Unfavorited button: PF viewBox `0 0 576 512` (FontAwesome star), PFJ viewBox `0 0 32 32` (Red Hat star) + +#### P3: Attribute Differences +- PF has `aria-label="not starred"` (lowercase), PFJ has `aria-label="Not starred"` (capitalized) + +## Action Items + +1. **[P1] Add variation:** Custom component — PF has a 'Custom component' section demonstrating button rendered as a custom element; PFJ has no equivalent section +2. **[P3] Fix attribute:** Hamburger aria-expanded — PF hamburger button has `aria-expanded="false"`, PFJ has none; add `aria-expanded` attribute toggled on open/close +3. **[P3] Fix attribute:** Favorite aria-label casing — PFJ capitalizes aria-label ("Not starred"/"Starred"), PF uses lowercase; align casing with PF +4. **[P4] Fix icon:** Favorite icon set — PFJ uses Red Hat star icon (viewBox `0 0 32 32`), PF uses FontAwesome star (viewBox `0 0 576 512`); switch to FontAwesome for consistency diff --git a/.claude/skills/pf-compare/examples/card.md b/.claude/skills/pf-compare/examples/card.md new file mode 100644 index 000000000..6acb08c44 --- /dev/null +++ b/.claude/skills/pf-compare/examples/card.md @@ -0,0 +1,61 @@ +--- +component: card +date: 2026-06-08 +pf_version: Release 6.5.1 +pf_url: https://www.patternfly.org/components/card +pfj_url: http://localhost:1234/components/card +sections: + pf_count: 22 + pfj_count: 7 + matched: 6 + missing_in_pfj: + - Card with subtitle and Actions + - Modifiers + - Header images and actions + - Title inline with images and actions + - Card header without title + - Card with header that wraps + - With HTML heading element + - With multiple body sections + - With a primary body section that fills + - Selectable + - Single selectable + - Actionable + - Actionable and selectable + - Expandable cards + - Expandable with icon + - Card with dividers + extra_in_pfj: [] +--- + +# PF Compare: card + +## Section Coverage + +| # | PF Section | PFJ Section | Group | Status | +|---|------------|-------------|-------|--------| +| 1 | Basic cards | Basic card | Examples | matched | +| 2 | Card with subtitle | Card with subtitle | Examples | matched | +| 3 | Card with subtitle and Actions | --- | Examples | missing_in_pfj | +| 4 | Secondary cards | Secondary card | Examples | matched | +| 5 | Modifiers | --- | Examples | missing_in_pfj | +| ... | ... | ... | ... | ... | + +## DOM Differences + +### Basic cards + +**Status:** ok + +### Card with subtitle + +**Status:** differences_found + +#### P2: Modifier Differences +- `pf-m-no-fill` on `div.pf-v6-c-card__body`: present in PF, absent in PFJ + +## Action Items + +1. **[P1] Add variation:** Card with subtitle and Actions — implement card header with actions dropdown +2. **[P1] Add variation:** Modifiers — implement modifier showcase (compact, flat, rounded, etc.) +3. **[P2] Fix CSS:** Card body no-fill — add `pf-m-no-fill` modifier support to card body diff --git a/.claude/skills/pf-compare/references/detect-pf-version.js b/.claude/skills/pf-compare/references/detect-pf-version.js new file mode 100644 index 000000000..5123e5daf --- /dev/null +++ b/.claude/skills/pf-compare/references/detect-pf-version.js @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Red Hat + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Detects the PatternFly version from the PF showcase site navigation. +// Looks for a button element whose text contains "Release" (e.g., "Release 6.1"). +// Falls back to "unknown" if not found -- this can happen if PF changes +// their nav layout or removes the release button. +() => { + const allButtons = document.querySelectorAll('button'); + const releaseBtn = Array.from(allButtons).find(b => b.textContent.includes('Release')); + if (releaseBtn) return releaseBtn.textContent.trim(); + const versionMeta = document.querySelector('meta[name="version"]'); + if (versionMeta) return versionMeta.getAttribute('content'); + const footerVersion = document.querySelector('footer')?.textContent?.match(/v?\d+\.\d+/); + if (footerVersion) return footerVersion[0]; + return 'unknown'; +} diff --git a/.claude/skills/pf-compare/references/extract-sections.js b/.claude/skills/pf-compare/references/extract-sections.js new file mode 100644 index 000000000..09ea92998 --- /dev/null +++ b/.claude/skills/pf-compare/references/extract-sections.js @@ -0,0 +1,81 @@ +/* + * Copyright 2023 Red Hat + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Extracts example sections from the jump links navigation on both +// PF React and PFJ showcase pages. Jump links follow a nested structure: +// top-level groups (H2 headings like "Examples", "Props") contain +// nested section items (H3 headings like "Variant examples", "Disabled buttons"). +// +// Usage: pass "pf" or "pfj" as the first argument to control which +// top-level groups are excluded. For "pf", pass the component slug +// as the second argument to validate that each section has a +// corresponding preview container (ws-react-c-{component}-{id}). +// +// Returns: [{ id, title, group }] +(pageType, componentSlug) => { + const PF_EXCLUDE = ['props', 'css variables']; + const PFJ_EXCLUDE = ['api documentation']; + const excludeGroups = pageType === 'pf' ? PF_EXCLUDE : PFJ_EXCLUDE; + + const jumpLinksList = document.querySelector('.pf-v6-c-jump-links__list'); + if (!jumpLinksList) return []; + + const sections = []; + const topItems = jumpLinksList.querySelectorAll(':scope > .pf-v6-c-jump-links__item'); + + for (const topItem of topItems) { + const topLink = topItem.querySelector(':scope > .pf-v6-c-jump-links__link'); + if (!topLink) continue; + + const groupTitle = topLink.textContent.trim(); + const groupHref = topLink.querySelector('a')?.getAttribute('href') || ''; + const groupId = groupHref.replace(/^#/, ''); + + if (excludeGroups.some(ex => groupTitle.toLowerCase() === ex)) continue; + + const nestedList = topItem.querySelector(':scope > .pf-v6-c-jump-links__list'); + if (nestedList) { + const nestedItems = nestedList.querySelectorAll(':scope > .pf-v6-c-jump-links__item'); + for (const nested of nestedItems) { + const link = nested.querySelector('a'); + if (!link) continue; + const title = link.textContent.trim(); + const href = link.getAttribute('href') || ''; + const id = href.replace(/^#/, ''); + sections.push({ id, title, group: groupTitle }); + } + } else { + sections.push({ id: groupId, title: groupTitle, group: null }); + } + } + + // For PF pages, filter to sections that have a preview container + if (pageType === 'pf' && componentSlug) { + const prefix = 'ws-react-c-' + componentSlug + '-'; + return sections.filter(s => document.getElementById(prefix + s.id)); + } + + // For PFJ pages, filter to sections that have a data-pfj-demo element + if (pageType === 'pfj') { + return sections.filter(s => { + const heading = document.getElementById(s.id); + if (!heading) return false; + const stack = heading.parentElement?.parentElement?.parentElement; + return stack && stack.querySelector('[data-pfj-demo]'); + }); + } + + return sections; +} diff --git a/.claude/skills/pf-compare/references/ignore-patterns.md b/.claude/skills/pf-compare/references/ignore-patterns.md new file mode 100644 index 000000000..c69c1020d --- /dev/null +++ b/.claude/skills/pf-compare/references/ignore-patterns.md @@ -0,0 +1,38 @@ +# Ignore Patterns for DOM Comparison + +Patterns to ignore when comparing PF and PFJ HTML output. The `normalize-dom.js` script handles most filtering during extraction. + +## Stripped by `normalize-dom.js` + +These are filtered out during normalization and never reach the comparison step: + +- **Non-PF classes** — only `pf-v6-c-*`, `pf-v6-l-*`, and `pf-m-*` classes are kept +- **Dynamic IDs** — all `id` attributes are stripped +- **Style attributes** — `style` is stripped +- **OUIA attributes** — `data-ouia-*` (test automation attributes) +- **Elemento attributes** — `data-pfcsce`, `on-detach-uid` +- **SVG internals** — SVG elements are collapsed to `{ tag: "svg", svgViewBox: "..." }`; no path data, nested groups, or defs are compared + +## Kept by `normalize-dom.js` + +These survive normalization and are available for comparison: + +- **PF classes** — `pf-v6-c-*` (component), `pf-v6-l-*` (layout) +- **PF modifiers** — `pf-m-*` +- **Semantic attributes** — `role`, `type`, `tabindex`, `disabled`, `hidden`, `href`, `target` +- **ARIA attributes** — all `aria-*` attributes +- **SVG viewBox** — used to detect different icons (e.g., `0 0 20 20` vs `0 0 32 32`) +- **Direct text content** — truncated to 50 characters + +## AI Comparison Guidelines + +The AI comparison step interprets the normalized element lists. When classifying differences: + +- **Whitespace and formatting** — not visible in normalized output, ignore +- **Element count differences** — may indicate showcase layout choices (PF often wraps examples in flex containers), not missing features +- **Icon viewBox differences** — usually means different icon sets (FontAwesome vs Red Hat icons), classify as P4 +- **Text content differences** — often showcase placeholder text, classify as P5 unless it indicates a functional difference + +## Version-Dependent Constants + +The `pf-v6-` class prefix and `ws-react-c-` ID prefix are tied to PatternFly v6. If the project upgrades to a new major version, update these prefixes in the extraction and normalization scripts. diff --git a/.claude/skills/pf-compare/references/normalize-dom.js b/.claude/skills/pf-compare/references/normalize-dom.js new file mode 100644 index 000000000..eb40c7a0e --- /dev/null +++ b/.claude/skills/pf-compare/references/normalize-dom.js @@ -0,0 +1,127 @@ +/* + * Copyright 2023 Red Hat + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Generic DOM normalizer for comparing PatternFly component HTML. +// Takes a raw HTML string (innerHTML of a preview container) and +// returns a flat list of element signatures containing only +// PF-relevant classes, modifiers, and attributes. +// +// This script is component-agnostic — it works for any PF component. +// +// Usage: pass the raw innerHTML string as the sole argument. +// Returns: [{ depth, tag, classes, modifiers, attributes, text, isSvg, svgViewBox }] +(html) => { + const container = document.createElement('div'); + container.innerHTML = html; + + const STRIP_ATTRS = new Set([ + 'class', 'style', 'data-pfcsce', 'on-detach-uid' + ]); + const KEEP_ATTRS = new Set([ + 'role', 'type', 'tabindex', 'disabled', 'hidden', + 'href', 'target' + ]); + + function shouldKeepAttr(name) { + if (STRIP_ATTRS.has(name)) return false; + if (KEEP_ATTRS.has(name)) return true; + if (name.startsWith('aria-')) return true; + if (name.startsWith('data-ouia-')) return false; + if (name === 'id') return false; + return false; + } + + const elements = []; + + function walk(node, depth) { + if (node.nodeType !== Node.ELEMENT_NODE) return; + + const tag = node.tagName.toLowerCase(); + + // SVG: capture viewBox only, do not recurse into children + if (tag === 'svg') { + elements.push({ + depth, + tag: 'svg', + classes: [], + modifiers: [], + attributes: {}, + text: '', + isSvg: true, + svgViewBox: node.getAttribute('viewBox') || null + }); + return; + } + + // Extract PF component/layout classes and modifier classes + const classes = []; + const modifiers = []; + for (const cls of node.classList) { + if (cls.startsWith('pf-v6-c-') || cls.startsWith('pf-v6-l-')) { + classes.push(cls); + } else if (cls.startsWith('pf-m-')) { + modifiers.push(cls); + } + } + classes.sort(); + modifiers.sort(); + + // Extract filtered attributes + const attributes = {}; + for (const attr of node.attributes) { + if (shouldKeepAttr(attr.name)) { + attributes[attr.name] = attr.value; + } + } + + // Direct text content (not descendant text) + let text = ''; + for (const child of node.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + const t = child.textContent.trim(); + if (t) text += (text ? ' ' : '') + t; + } + } + if (text.length > 50) text = text.substring(0, 50) + '...'; + + // Only include elements that have PF classes, modifiers, or relevant attributes + const hasPfContent = classes.length > 0 || modifiers.length > 0; + const hasRelevantAttrs = Object.keys(attributes).length > 0; + if (hasPfContent || hasRelevantAttrs || tag === 'input' || tag === 'select' || tag === 'textarea') { + elements.push({ + depth, + tag, + classes, + modifiers, + attributes, + text, + isSvg: false, + svgViewBox: null + }); + } + + // Recurse into children + for (const child of node.children) { + walk(child, depth + 1); + } + } + + // Walk top-level children of the container + for (const child of container.children) { + walk(child, 0); + } + + return elements; +} diff --git a/.claude/skills/pf-compare/references/report-schema.json b/.claude/skills/pf-compare/references/report-schema.json new file mode 100644 index 000000000..c5f0e07be --- /dev/null +++ b/.claude/skills/pf-compare/references/report-schema.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "PF Compare Report", + "description": "Comparison report between a PatternFly component and its PatternFly Java implementation.", + "type": "object", + "required": ["skillVersion", "component", "date", "pfVersion", "pfUrl", "pfjUrl", "sections", "variations", "actionItems"], + "properties": { + "skillVersion": { + "type": "string", + "description": "Version of the pf-compare skill that generated this report (from SKILL.md metadata.version)" + }, + "component": { + "type": "string", + "description": "Component URL slug (e.g., 'button', 'data-list')" + }, + "date": { + "type": "string", + "format": "date", + "description": "Report generation date (YYYY-MM-DD)" + }, + "pfVersion": { + "type": "string", + "description": "PatternFly version detected from the PF site (e.g., 'Release 6.5.1')" + }, + "pfUrl": { + "type": "string", + "format": "uri", + "description": "PF React showcase URL (e.g., https://www.patternfly.org/components/button)" + }, + "pfjUrl": { + "type": "string", + "format": "uri", + "description": "PFJ showcase URL (e.g., http://localhost:1234/components/button)" + }, + "sections": { + "type": "object", + "description": "Section coverage summary", + "required": ["pfCount", "pfjCount", "matched", "missingInPfj", "extraInPfj"], + "properties": { + "pfCount": { "type": "integer", "description": "Number of example sections in PF" }, + "pfjCount": { "type": "integer", "description": "Number of example sections in PFJ" }, + "matched": { "type": "integer", "description": "Number of matched section pairs" }, + "missingInPfj": { + "type": "array", + "items": { "type": "string" }, + "description": "PF section titles with no PFJ counterpart" + }, + "extraInPfj": { + "type": "array", + "items": { "type": "string" }, + "description": "PFJ section titles with no PF counterpart" + } + } + }, + "variations": { + "type": "array", + "description": "PF section data with raw HTML (consumed by pf-align for reference HTML)", + "items": { + "type": "object", + "required": ["slug", "title", "html"], + "properties": { + "slug": { "type": "string", "description": "Section URL slug from heading ID" }, + "title": { "type": "string", "description": "Section heading title" }, + "html": { "type": "string", "description": "Raw innerHTML of the PF preview container" } + } + } + }, + "actionItems": { + "type": "array", + "description": "Prioritized list of differences to address", + "items": { + "type": "object", + "required": ["number", "type", "priority", "title", "description"], + "properties": { + "number": { "type": "integer", "description": "Sequential action item number" }, + "type": { + "type": "string", + "enum": ["add_variation", "fix_css", "fix_structure", "fix_attribute", "fix_icon", "implement_feature"], + "description": "Type of action required" + }, + "priority": { + "type": "string", + "enum": ["P1", "P2", "P3", "P4", "P5"], + "description": "P1=missing elements, P2=modifiers, P3=attributes, P4=icons, P5=cosmetic" + }, + "title": { "type": "string", "description": "Short title identifying the target" }, + "description": { "type": "string", "description": "What needs to be done and why" }, + "category": { + "type": "string", + "enum": ["missing_sections", "dom_differences", "feature_implementation"], + "description": "Grouping category" + }, + "variations": { + "type": "array", + "items": { "type": "string" }, + "description": "Section titles affected by this action item" + } + } + } + } + } +} diff --git a/.claude/skills/pf-compare/references/report-template.md b/.claude/skills/pf-compare/references/report-template.md new file mode 100644 index 000000000..bbb84d865 --- /dev/null +++ b/.claude/skills/pf-compare/references/report-template.md @@ -0,0 +1,60 @@ +# Report Template + +Use this template when generating the markdown comparison report. A companion JSON file (`.json`) is written alongside this markdown report using the schema from `report-schema.json`. + +```markdown +--- +component: +date: +pf_version: +pf_url: +pfj_url: +sections: + pf_count: + pfj_count: + matched: + missing_in_pfj: + - title1 + - title2 + extra_in_pfj: + - title1 +--- + +# PF Compare: + +## Section Coverage + +| # | PF Section | PFJ Section | Group | Status | +|---|------------|-------------|-------|--------| +| 1 | PF title | PFJ title | Examples | matched | +| 2 | PF title | --- | Examples | missing_in_pfj | + +## DOM Differences + +###
+ +**Status:** ok | differences_found + +#### P1: Missing Component Elements +- `pf-v6-c-button__icon` — present in PF, absent in PFJ + +#### P2: Modifier Differences +- `pf-m-danger` on `