diff --git a/.agents/skills/adapt/SKILL.md b/.agents/skills/adapt/SKILL.md index 0bf65f30a..9a0703af0 100644 --- a/.agents/skills/adapt/SKILL.md +++ b/.agents/skills/adapt/SKILL.md @@ -2,7 +2,7 @@ name: adapt description: Adapt designs to work across different screen sizes, devices, contexts, or platforms. Ensures consistent experience across varied environments. user-invocable: true -argument-hint: [TARGET=] [CONTEXT=] +argument-hint: "[target] [context (mobile, tablet, print...)]" --- Adapt existing designs to work effectively across different contexts - different screen sizes, devices, platforms, or use cases. diff --git a/.agents/skills/animate/SKILL.md b/.agents/skills/animate/SKILL.md index 3a8b2380b..420f9863e 100644 --- a/.agents/skills/animate/SKILL.md +++ b/.agents/skills/animate/SKILL.md @@ -2,7 +2,7 @@ name: animate description: Review a feature and enhance it with purposeful animations, micro-interactions, and motion effects that improve usability and delight. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight. diff --git a/.agents/skills/arrange/SKILL.md b/.agents/skills/arrange/SKILL.md index 255055c01..2f22f3faa 100644 --- a/.agents/skills/arrange/SKILL.md +++ b/.agents/skills/arrange/SKILL.md @@ -2,7 +2,7 @@ name: arrange description: Improve layout, spacing, and visual rhythm. Fixes monotonous grids, inconsistent spacing, and weak visual hierarchy to create intentional compositions. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions. diff --git a/.agents/skills/audit/SKILL.md b/.agents/skills/audit/SKILL.md index 74cbb7475..f11131ae0 100644 --- a/.agents/skills/audit/SKILL.md +++ b/.agents/skills/audit/SKILL.md @@ -2,7 +2,7 @@ name: audit description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations. user-invocable: true -argument-hint: [AREA=] +argument-hint: "[area (feature, page, component...)]" --- Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address. diff --git a/.agents/skills/bolder/SKILL.md b/.agents/skills/bolder/SKILL.md index 77e78e703..d375ad1f6 100644 --- a/.agents/skills/bolder/SKILL.md +++ b/.agents/skills/bolder/SKILL.md @@ -2,7 +2,7 @@ name: bolder description: Amplify safe or boring designs to make them more visually interesting and stimulating. Increases impact while maintaining usability. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Increase visual impact and personality in designs that are too safe, generic, or visually underwhelming, creating more engaging and memorable experiences. diff --git a/.agents/skills/clarify/SKILL.md b/.agents/skills/clarify/SKILL.md index 86814d2c2..f0f602fa4 100644 --- a/.agents/skills/clarify/SKILL.md +++ b/.agents/skills/clarify/SKILL.md @@ -2,7 +2,7 @@ name: clarify description: Improve unclear UX copy, error messages, microcopy, labels, and instructions. Makes interfaces easier to understand and use. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use. diff --git a/.agents/skills/colorize/SKILL.md b/.agents/skills/colorize/SKILL.md index beb8afd0d..7a72c3e57 100644 --- a/.agents/skills/colorize/SKILL.md +++ b/.agents/skills/colorize/SKILL.md @@ -2,7 +2,7 @@ name: colorize description: Add strategic color to features that are too monochromatic or lack visual interest. Makes interfaces more engaging and expressive. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Strategically introduce color to designs that are too monochromatic, gray, or lacking in visual warmth and personality. diff --git a/.agents/skills/critique/SKILL.md b/.agents/skills/critique/SKILL.md index f25717cab..62e2552fd 100644 --- a/.agents/skills/critique/SKILL.md +++ b/.agents/skills/critique/SKILL.md @@ -2,7 +2,7 @@ name: critique description: Evaluate design effectiveness from a UX perspective. Assesses visual hierarchy, information architecture, emotional resonance, and overall design quality with actionable feedback. user-invocable: true -argument-hint: [AREA=] +argument-hint: "[area (feature, page, component...)]" --- ## MANDATORY PREPARATION diff --git a/.agents/skills/delight/SKILL.md b/.agents/skills/delight/SKILL.md index 2d92c285c..9eb1edaf1 100644 --- a/.agents/skills/delight/SKILL.md +++ b/.agents/skills/delight/SKILL.md @@ -2,7 +2,7 @@ name: delight description: Add moments of joy, personality, and unexpected touches that make interfaces memorable and enjoyable to use. Elevates functional to delightful. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify opportunities to add moments of joy, personality, and unexpected polish that transform functional interfaces into delightful experiences. diff --git a/.agents/skills/distill/SKILL.md b/.agents/skills/distill/SKILL.md index 521bddb87..8d7a50faf 100644 --- a/.agents/skills/distill/SKILL.md +++ b/.agents/skills/distill/SKILL.md @@ -2,7 +2,7 @@ name: distill description: Strip designs to their essence by removing unnecessary complexity. Great design is simple, powerful, and clean. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Remove unnecessary complexity from designs, revealing the essential elements and creating clarity through ruthless simplification. diff --git a/.agents/skills/extract/SKILL.md b/.agents/skills/extract/SKILL.md index 57019a9d2..98f62a04c 100644 --- a/.agents/skills/extract/SKILL.md +++ b/.agents/skills/extract/SKILL.md @@ -2,7 +2,7 @@ name: extract description: Extract and consolidate reusable components, design tokens, and patterns into your design system. Identifies opportunities for systematic reuse and enriches your component library. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse. diff --git a/.agents/skills/harden/SKILL.md b/.agents/skills/harden/SKILL.md index 36dc972ab..f901773a6 100644 --- a/.agents/skills/harden/SKILL.md +++ b/.agents/skills/harden/SKILL.md @@ -2,7 +2,7 @@ name: harden description: Improve interface resilience through better error handling, i18n support, text overflow handling, and edge case management. Makes interfaces robust and production-ready. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Strengthen interfaces against edge cases, errors, internationalization issues, and real-world usage scenarios that break idealized designs. diff --git a/.agents/skills/normalize/SKILL.md b/.agents/skills/normalize/SKILL.md index 5106b9724..f8af99752 100644 --- a/.agents/skills/normalize/SKILL.md +++ b/.agents/skills/normalize/SKILL.md @@ -2,7 +2,7 @@ name: normalize description: Normalize design to match your design system and ensure consistency user-invocable: true -argument-hint: [FEATURE=] +argument-hint: "[feature (page, route, component...)]" --- Analyze and redesign the feature to perfectly match our design system standards, aesthetics, and established patterns. diff --git a/.agents/skills/onboard/SKILL.md b/.agents/skills/onboard/SKILL.md index 966a1cda9..10f3618b1 100644 --- a/.agents/skills/onboard/SKILL.md +++ b/.agents/skills/onboard/SKILL.md @@ -2,7 +2,7 @@ name: onboard description: Design or improve onboarding flows, empty states, and first-time user experiences. Helps users get started successfully and understand value quickly. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.agents/skills/optimize/SKILL.md b/.agents/skills/optimize/SKILL.md index 661ab952b..3f76667f6 100644 --- a/.agents/skills/optimize/SKILL.md +++ b/.agents/skills/optimize/SKILL.md @@ -2,7 +2,7 @@ name: optimize description: Improve interface performance across loading speed, rendering, animations, images, and bundle size. Makes experiences faster and smoother. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify and fix performance issues to create faster, smoother user experiences. diff --git a/.agents/skills/overdrive/SKILL.md b/.agents/skills/overdrive/SKILL.md index 843c4ce45..36dfa8b87 100644 --- a/.agents/skills/overdrive/SKILL.md +++ b/.agents/skills/overdrive/SKILL.md @@ -2,7 +2,7 @@ name: overdrive description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?" user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Start your response with: diff --git a/.agents/skills/polish/SKILL.md b/.agents/skills/polish/SKILL.md index b6e75ac54..fe9e27687 100644 --- a/.agents/skills/polish/SKILL.md +++ b/.agents/skills/polish/SKILL.md @@ -2,7 +2,7 @@ name: polish description: Final quality pass before shipping. Fixes alignment, spacing, consistency, and detail issues that separate good from great. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.agents/skills/quieter/SKILL.md b/.agents/skills/quieter/SKILL.md index 3fa7819a4..a542c3ee9 100644 --- a/.agents/skills/quieter/SKILL.md +++ b/.agents/skills/quieter/SKILL.md @@ -2,7 +2,7 @@ name: quieter description: Tone down overly bold or visually aggressive designs. Reduces intensity while maintaining design quality and impact. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Reduce visual intensity in designs that are too bold, aggressive, or overstimulating, creating a more refined and approachable aesthetic without losing effectiveness. diff --git a/.agents/skills/typeset/SKILL.md b/.agents/skills/typeset/SKILL.md index 50c2e3998..d331e2745 100644 --- a/.agents/skills/typeset/SKILL.md +++ b/.agents/skills/typeset/SKILL.md @@ -2,7 +2,7 @@ name: typeset description: Improve typography by fixing font choices, hierarchy, sizing, weight consistency, and readability. Makes text feel intentional and polished. user-invocable: true -argument-hint: [TARGET=] +argument-hint: "[target]" --- Assess and improve typography that feels generic, inconsistent, or poorly structured — turning default-looking text into intentional, well-crafted type. diff --git a/.claude/skills/adapt/SKILL.md b/.claude/skills/adapt/SKILL.md index 9d46f289a..9a0703af0 100644 --- a/.claude/skills/adapt/SKILL.md +++ b/.claude/skills/adapt/SKILL.md @@ -2,13 +2,7 @@ name: adapt description: Adapt designs to work across different screen sizes, devices, contexts, or platforms. Ensures consistent experience across varied environments. user-invocable: true -args: - - name: target - description: The feature or component to adapt (optional) - required: false - - name: context - description: What to adapt for (mobile, tablet, desktop, print, email, etc.) - required: false +argument-hint: "[target] [context (mobile, tablet, print...)]" --- Adapt existing designs to work effectively across different contexts - different screen sizes, devices, platforms, or use cases. diff --git a/.claude/skills/animate/SKILL.md b/.claude/skills/animate/SKILL.md index 182b93e90..f1d74000d 100644 --- a/.claude/skills/animate/SKILL.md +++ b/.claude/skills/animate/SKILL.md @@ -2,10 +2,7 @@ name: animate description: Review a feature and enhance it with purposeful animations, micro-interactions, and motion effects that improve usability and delight. user-invocable: true -args: - - name: target - description: The feature or component to animate (optional) - required: false +argument-hint: "[target]" --- Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight. diff --git a/.claude/skills/arrange/SKILL.md b/.claude/skills/arrange/SKILL.md index 93474d0f6..2f22f3faa 100644 --- a/.claude/skills/arrange/SKILL.md +++ b/.claude/skills/arrange/SKILL.md @@ -2,10 +2,7 @@ name: arrange description: Improve layout, spacing, and visual rhythm. Fixes monotonous grids, inconsistent spacing, and weak visual hierarchy to create intentional compositions. user-invocable: true -args: - - name: target - description: The feature or component to improve layout for (optional) - required: false +argument-hint: "[target]" --- Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions. diff --git a/.claude/skills/audit/SKILL.md b/.claude/skills/audit/SKILL.md index 9f2d8b9d7..f11131ae0 100644 --- a/.claude/skills/audit/SKILL.md +++ b/.claude/skills/audit/SKILL.md @@ -2,10 +2,7 @@ name: audit description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations. user-invocable: true -args: - - name: area - description: The feature or area to audit (optional) - required: false +argument-hint: "[area (feature, page, component...)]" --- Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address. diff --git a/.claude/skills/bolder/SKILL.md b/.claude/skills/bolder/SKILL.md index adcba4f71..19c37abc1 100644 --- a/.claude/skills/bolder/SKILL.md +++ b/.claude/skills/bolder/SKILL.md @@ -2,10 +2,7 @@ name: bolder description: Amplify safe or boring designs to make them more visually interesting and stimulating. Increases impact while maintaining usability. user-invocable: true -args: - - name: target - description: The feature or component to make bolder (optional) - required: false +argument-hint: "[target]" --- Increase visual impact and personality in designs that are too safe, generic, or visually underwhelming, creating more engaging and memorable experiences. diff --git a/.claude/skills/clarify/SKILL.md b/.claude/skills/clarify/SKILL.md index e6a859286..f0f602fa4 100644 --- a/.claude/skills/clarify/SKILL.md +++ b/.claude/skills/clarify/SKILL.md @@ -2,10 +2,7 @@ name: clarify description: Improve unclear UX copy, error messages, microcopy, labels, and instructions. Makes interfaces easier to understand and use. user-invocable: true -args: - - name: target - description: The feature or component with unclear copy (optional) - required: false +argument-hint: "[target]" --- Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use. diff --git a/.claude/skills/colorize/SKILL.md b/.claude/skills/colorize/SKILL.md index 0bfc3731a..e7424d47a 100644 --- a/.claude/skills/colorize/SKILL.md +++ b/.claude/skills/colorize/SKILL.md @@ -2,10 +2,7 @@ name: colorize description: Add strategic color to features that are too monochromatic or lack visual interest. Makes interfaces more engaging and expressive. user-invocable: true -args: - - name: target - description: The feature or component to colorize (optional) - required: false +argument-hint: "[target]" --- Strategically introduce color to designs that are too monochromatic, gray, or lacking in visual warmth and personality. diff --git a/.claude/skills/critique/SKILL.md b/.claude/skills/critique/SKILL.md index bf735c842..62e2552fd 100644 --- a/.claude/skills/critique/SKILL.md +++ b/.claude/skills/critique/SKILL.md @@ -2,10 +2,7 @@ name: critique description: Evaluate design effectiveness from a UX perspective. Assesses visual hierarchy, information architecture, emotional resonance, and overall design quality with actionable feedback. user-invocable: true -args: - - name: area - description: The feature or area to critique (optional) - required: false +argument-hint: "[area (feature, page, component...)]" --- ## MANDATORY PREPARATION diff --git a/.claude/skills/delight/SKILL.md b/.claude/skills/delight/SKILL.md index 644bacb55..0b10c822d 100644 --- a/.claude/skills/delight/SKILL.md +++ b/.claude/skills/delight/SKILL.md @@ -2,10 +2,7 @@ name: delight description: Add moments of joy, personality, and unexpected touches that make interfaces memorable and enjoyable to use. Elevates functional to delightful. user-invocable: true -args: - - name: target - description: The feature or area to add delight to (optional) - required: false +argument-hint: "[target]" --- Identify opportunities to add moments of joy, personality, and unexpected polish that transform functional interfaces into delightful experiences. diff --git a/.claude/skills/distill/SKILL.md b/.claude/skills/distill/SKILL.md index 2c168904f..b8d2b86f6 100644 --- a/.claude/skills/distill/SKILL.md +++ b/.claude/skills/distill/SKILL.md @@ -2,10 +2,7 @@ name: distill description: Strip designs to their essence by removing unnecessary complexity. Great design is simple, powerful, and clean. user-invocable: true -args: - - name: target - description: The feature or component to distill (optional) - required: false +argument-hint: "[target]" --- Remove unnecessary complexity from designs, revealing the essential elements and creating clarity through ruthless simplification. diff --git a/.claude/skills/extract/SKILL.md b/.claude/skills/extract/SKILL.md index ca1b8cd81..98f62a04c 100644 --- a/.claude/skills/extract/SKILL.md +++ b/.claude/skills/extract/SKILL.md @@ -2,10 +2,7 @@ name: extract description: Extract and consolidate reusable components, design tokens, and patterns into your design system. Identifies opportunities for systematic reuse and enriches your component library. user-invocable: true -args: - - name: target - description: The feature, component, or area to extract from (optional) - required: false +argument-hint: "[target]" --- Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse. diff --git a/.claude/skills/harden/SKILL.md b/.claude/skills/harden/SKILL.md index 867842183..f901773a6 100644 --- a/.claude/skills/harden/SKILL.md +++ b/.claude/skills/harden/SKILL.md @@ -2,10 +2,7 @@ name: harden description: Improve interface resilience through better error handling, i18n support, text overflow handling, and edge case management. Makes interfaces robust and production-ready. user-invocable: true -args: - - name: target - description: The feature or area to harden (optional) - required: false +argument-hint: "[target]" --- Strengthen interfaces against edge cases, errors, internationalization issues, and real-world usage scenarios that break idealized designs. diff --git a/.claude/skills/normalize/SKILL.md b/.claude/skills/normalize/SKILL.md index 8a8f92356..f8af99752 100644 --- a/.claude/skills/normalize/SKILL.md +++ b/.claude/skills/normalize/SKILL.md @@ -2,10 +2,7 @@ name: normalize description: Normalize design to match your design system and ensure consistency user-invocable: true -args: - - name: feature - description: The page, route, or feature to normalize (optional) - required: false +argument-hint: "[feature (page, route, component...)]" --- Analyze and redesign the feature to perfectly match our design system standards, aesthetics, and established patterns. diff --git a/.claude/skills/onboard/SKILL.md b/.claude/skills/onboard/SKILL.md index 12d246dd8..10f3618b1 100644 --- a/.claude/skills/onboard/SKILL.md +++ b/.claude/skills/onboard/SKILL.md @@ -2,10 +2,7 @@ name: onboard description: Design or improve onboarding flows, empty states, and first-time user experiences. Helps users get started successfully and understand value quickly. user-invocable: true -args: - - name: target - description: The feature or area needing onboarding (optional) - required: false +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.claude/skills/optimize/SKILL.md b/.claude/skills/optimize/SKILL.md index 7240fa9aa..3f76667f6 100644 --- a/.claude/skills/optimize/SKILL.md +++ b/.claude/skills/optimize/SKILL.md @@ -2,10 +2,7 @@ name: optimize description: Improve interface performance across loading speed, rendering, animations, images, and bundle size. Makes experiences faster and smoother. user-invocable: true -args: - - name: target - description: The feature or area to optimize (optional) - required: false +argument-hint: "[target]" --- Identify and fix performance issues to create faster, smoother user experiences. diff --git a/.claude/skills/overdrive/SKILL.md b/.claude/skills/overdrive/SKILL.md index 61817077d..3011b3c21 100644 --- a/.claude/skills/overdrive/SKILL.md +++ b/.claude/skills/overdrive/SKILL.md @@ -2,10 +2,7 @@ name: overdrive description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?" user-invocable: true -args: - - name: target - description: The feature or area to push into overdrive (optional) - required: false +argument-hint: "[target]" --- Start your response with: diff --git a/.claude/skills/polish/SKILL.md b/.claude/skills/polish/SKILL.md index f8b8efbad..fe9e27687 100644 --- a/.claude/skills/polish/SKILL.md +++ b/.claude/skills/polish/SKILL.md @@ -2,10 +2,7 @@ name: polish description: Final quality pass before shipping. Fixes alignment, spacing, consistency, and detail issues that separate good from great. user-invocable: true -args: - - name: target - description: The feature or area to polish (optional) - required: false +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.claude/skills/quieter/SKILL.md b/.claude/skills/quieter/SKILL.md index b99ed5372..8cc092848 100644 --- a/.claude/skills/quieter/SKILL.md +++ b/.claude/skills/quieter/SKILL.md @@ -2,10 +2,7 @@ name: quieter description: Tone down overly bold or visually aggressive designs. Reduces intensity while maintaining design quality and impact. user-invocable: true -args: - - name: target - description: The feature or component to make quieter (optional) - required: false +argument-hint: "[target]" --- Reduce visual intensity in designs that are too bold, aggressive, or overstimulating, creating a more refined and approachable aesthetic without losing effectiveness. diff --git a/.claude/skills/typeset/SKILL.md b/.claude/skills/typeset/SKILL.md index 246b99ef0..d331e2745 100644 --- a/.claude/skills/typeset/SKILL.md +++ b/.claude/skills/typeset/SKILL.md @@ -2,10 +2,7 @@ name: typeset description: Improve typography by fixing font choices, hierarchy, sizing, weight consistency, and readability. Makes text feel intentional and polished. user-invocable: true -args: - - name: target - description: The feature or component to improve typography for (optional) - required: false +argument-hint: "[target]" --- Assess and improve typography that feels generic, inconsistent, or poorly structured — turning default-looking text into intentional, well-crafted type. diff --git a/.codex/skills/adapt/SKILL.md b/.codex/skills/adapt/SKILL.md index d6fa63430..0a5403f8b 100644 --- a/.codex/skills/adapt/SKILL.md +++ b/.codex/skills/adapt/SKILL.md @@ -1,7 +1,7 @@ --- name: adapt description: Adapt designs to work across different screen sizes, devices, contexts, or platforms. Ensures consistent experience across varied environments. -argument-hint: [TARGET=] [CONTEXT=] +argument-hint: "[target] [context (mobile, tablet, print...)]" --- Adapt existing designs to work effectively across different contexts - different screen sizes, devices, platforms, or use cases. diff --git a/.codex/skills/animate/SKILL.md b/.codex/skills/animate/SKILL.md index 4d7a9d83e..52eb0bf5d 100644 --- a/.codex/skills/animate/SKILL.md +++ b/.codex/skills/animate/SKILL.md @@ -1,7 +1,7 @@ --- name: animate description: Review a feature and enhance it with purposeful animations, micro-interactions, and motion effects that improve usability and delight. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight. diff --git a/.codex/skills/arrange/SKILL.md b/.codex/skills/arrange/SKILL.md index a862918fc..27cad8e59 100644 --- a/.codex/skills/arrange/SKILL.md +++ b/.codex/skills/arrange/SKILL.md @@ -1,7 +1,7 @@ --- name: arrange description: Improve layout, spacing, and visual rhythm. Fixes monotonous grids, inconsistent spacing, and weak visual hierarchy to create intentional compositions. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions. diff --git a/.codex/skills/audit/SKILL.md b/.codex/skills/audit/SKILL.md index ac09ebc59..48ab66d54 100644 --- a/.codex/skills/audit/SKILL.md +++ b/.codex/skills/audit/SKILL.md @@ -1,7 +1,7 @@ --- name: audit description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations. -argument-hint: [AREA=] +argument-hint: "[area (feature, page, component...)]" --- Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address. diff --git a/.codex/skills/bolder/SKILL.md b/.codex/skills/bolder/SKILL.md index 79b09ba99..2680f00cb 100644 --- a/.codex/skills/bolder/SKILL.md +++ b/.codex/skills/bolder/SKILL.md @@ -1,7 +1,7 @@ --- name: bolder description: Amplify safe or boring designs to make them more visually interesting and stimulating. Increases impact while maintaining usability. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Increase visual impact and personality in designs that are too safe, generic, or visually underwhelming, creating more engaging and memorable experiences. diff --git a/.codex/skills/clarify/SKILL.md b/.codex/skills/clarify/SKILL.md index 5578658f2..88941b26e 100644 --- a/.codex/skills/clarify/SKILL.md +++ b/.codex/skills/clarify/SKILL.md @@ -1,7 +1,7 @@ --- name: clarify description: Improve unclear UX copy, error messages, microcopy, labels, and instructions. Makes interfaces easier to understand and use. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use. diff --git a/.codex/skills/colorize/SKILL.md b/.codex/skills/colorize/SKILL.md index 801bb4a4e..0e511001d 100644 --- a/.codex/skills/colorize/SKILL.md +++ b/.codex/skills/colorize/SKILL.md @@ -1,7 +1,7 @@ --- name: colorize description: Add strategic color to features that are too monochromatic or lack visual interest. Makes interfaces more engaging and expressive. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Strategically introduce color to designs that are too monochromatic, gray, or lacking in visual warmth and personality. diff --git a/.codex/skills/critique/SKILL.md b/.codex/skills/critique/SKILL.md index 615b3586b..6077c439a 100644 --- a/.codex/skills/critique/SKILL.md +++ b/.codex/skills/critique/SKILL.md @@ -1,7 +1,7 @@ --- name: critique description: Evaluate design effectiveness from a UX perspective. Assesses visual hierarchy, information architecture, emotional resonance, and overall design quality with actionable feedback. -argument-hint: [AREA=] +argument-hint: "[area (feature, page, component...)]" --- ## MANDATORY PREPARATION diff --git a/.codex/skills/delight/SKILL.md b/.codex/skills/delight/SKILL.md index 4f837f4fd..42ea11367 100644 --- a/.codex/skills/delight/SKILL.md +++ b/.codex/skills/delight/SKILL.md @@ -1,7 +1,7 @@ --- name: delight description: Add moments of joy, personality, and unexpected touches that make interfaces memorable and enjoyable to use. Elevates functional to delightful. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify opportunities to add moments of joy, personality, and unexpected polish that transform functional interfaces into delightful experiences. diff --git a/.codex/skills/distill/SKILL.md b/.codex/skills/distill/SKILL.md index fcfb05e64..7f3d9ff49 100644 --- a/.codex/skills/distill/SKILL.md +++ b/.codex/skills/distill/SKILL.md @@ -1,7 +1,7 @@ --- name: distill description: Strip designs to their essence by removing unnecessary complexity. Great design is simple, powerful, and clean. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Remove unnecessary complexity from designs, revealing the essential elements and creating clarity through ruthless simplification. diff --git a/.codex/skills/extract/SKILL.md b/.codex/skills/extract/SKILL.md index 42d77aa6e..ced714904 100644 --- a/.codex/skills/extract/SKILL.md +++ b/.codex/skills/extract/SKILL.md @@ -1,7 +1,7 @@ --- name: extract description: Extract and consolidate reusable components, design tokens, and patterns into your design system. Identifies opportunities for systematic reuse and enriches your component library. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse. diff --git a/.codex/skills/harden/SKILL.md b/.codex/skills/harden/SKILL.md index 154aa7356..e74bc150f 100644 --- a/.codex/skills/harden/SKILL.md +++ b/.codex/skills/harden/SKILL.md @@ -1,7 +1,7 @@ --- name: harden description: Improve interface resilience through better error handling, i18n support, text overflow handling, and edge case management. Makes interfaces robust and production-ready. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Strengthen interfaces against edge cases, errors, internationalization issues, and real-world usage scenarios that break idealized designs. diff --git a/.codex/skills/normalize/SKILL.md b/.codex/skills/normalize/SKILL.md index 014fa93c3..08ff9286a 100644 --- a/.codex/skills/normalize/SKILL.md +++ b/.codex/skills/normalize/SKILL.md @@ -1,7 +1,7 @@ --- name: normalize description: Normalize design to match your design system and ensure consistency -argument-hint: [FEATURE=] +argument-hint: "[feature (page, route, component...)]" --- Analyze and redesign the feature to perfectly match our design system standards, aesthetics, and established patterns. diff --git a/.codex/skills/onboard/SKILL.md b/.codex/skills/onboard/SKILL.md index 132d2ecaf..82ddb5874 100644 --- a/.codex/skills/onboard/SKILL.md +++ b/.codex/skills/onboard/SKILL.md @@ -1,7 +1,7 @@ --- name: onboard description: Design or improve onboarding flows, empty states, and first-time user experiences. Helps users get started successfully and understand value quickly. -argument-hint: [TARGET=] +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.codex/skills/optimize/SKILL.md b/.codex/skills/optimize/SKILL.md index 6b7d8a588..35417e9f6 100644 --- a/.codex/skills/optimize/SKILL.md +++ b/.codex/skills/optimize/SKILL.md @@ -1,7 +1,7 @@ --- name: optimize description: Improve interface performance across loading speed, rendering, animations, images, and bundle size. Makes experiences faster and smoother. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Identify and fix performance issues to create faster, smoother user experiences. diff --git a/.codex/skills/overdrive/SKILL.md b/.codex/skills/overdrive/SKILL.md index 9047fea9a..0b990230f 100644 --- a/.codex/skills/overdrive/SKILL.md +++ b/.codex/skills/overdrive/SKILL.md @@ -1,7 +1,7 @@ --- name: overdrive description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?" -argument-hint: [TARGET=] +argument-hint: "[target]" --- Start your response with: diff --git a/.codex/skills/polish/SKILL.md b/.codex/skills/polish/SKILL.md index 0e229290b..4f8dea20e 100644 --- a/.codex/skills/polish/SKILL.md +++ b/.codex/skills/polish/SKILL.md @@ -1,7 +1,7 @@ --- name: polish description: Final quality pass before shipping. Fixes alignment, spacing, consistency, and detail issues that separate good from great. -argument-hint: [TARGET=] +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.codex/skills/quieter/SKILL.md b/.codex/skills/quieter/SKILL.md index a5d5a5fb1..e4ba957f8 100644 --- a/.codex/skills/quieter/SKILL.md +++ b/.codex/skills/quieter/SKILL.md @@ -1,7 +1,7 @@ --- name: quieter description: Tone down overly bold or visually aggressive designs. Reduces intensity while maintaining design quality and impact. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Reduce visual intensity in designs that are too bold, aggressive, or overstimulating, creating a more refined and approachable aesthetic without losing effectiveness. diff --git a/.codex/skills/typeset/SKILL.md b/.codex/skills/typeset/SKILL.md index 899f7ad51..57a1d9dfc 100644 --- a/.codex/skills/typeset/SKILL.md +++ b/.codex/skills/typeset/SKILL.md @@ -1,7 +1,7 @@ --- name: typeset description: Improve typography by fixing font choices, hierarchy, sizing, weight consistency, and readability. Makes text feel intentional and polished. -argument-hint: [TARGET=] +argument-hint: "[target]" --- Assess and improve typography that feels generic, inconsistent, or poorly structured — turning default-looking text into intentional, well-crafted type. diff --git a/.opencode/skills/adapt/SKILL.md b/.opencode/skills/adapt/SKILL.md index 9d46f289a..9a0703af0 100644 --- a/.opencode/skills/adapt/SKILL.md +++ b/.opencode/skills/adapt/SKILL.md @@ -2,13 +2,7 @@ name: adapt description: Adapt designs to work across different screen sizes, devices, contexts, or platforms. Ensures consistent experience across varied environments. user-invocable: true -args: - - name: target - description: The feature or component to adapt (optional) - required: false - - name: context - description: What to adapt for (mobile, tablet, desktop, print, email, etc.) - required: false +argument-hint: "[target] [context (mobile, tablet, print...)]" --- Adapt existing designs to work effectively across different contexts - different screen sizes, devices, platforms, or use cases. diff --git a/.opencode/skills/animate/SKILL.md b/.opencode/skills/animate/SKILL.md index dd5c7bfe9..ef9497c3f 100644 --- a/.opencode/skills/animate/SKILL.md +++ b/.opencode/skills/animate/SKILL.md @@ -2,10 +2,7 @@ name: animate description: Review a feature and enhance it with purposeful animations, micro-interactions, and motion effects that improve usability and delight. user-invocable: true -args: - - name: target - description: The feature or component to animate (optional) - required: false +argument-hint: "[target]" --- Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight. diff --git a/.opencode/skills/arrange/SKILL.md b/.opencode/skills/arrange/SKILL.md index 93474d0f6..2f22f3faa 100644 --- a/.opencode/skills/arrange/SKILL.md +++ b/.opencode/skills/arrange/SKILL.md @@ -2,10 +2,7 @@ name: arrange description: Improve layout, spacing, and visual rhythm. Fixes monotonous grids, inconsistent spacing, and weak visual hierarchy to create intentional compositions. user-invocable: true -args: - - name: target - description: The feature or component to improve layout for (optional) - required: false +argument-hint: "[target]" --- Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions. diff --git a/.opencode/skills/audit/SKILL.md b/.opencode/skills/audit/SKILL.md index 9f2d8b9d7..f11131ae0 100644 --- a/.opencode/skills/audit/SKILL.md +++ b/.opencode/skills/audit/SKILL.md @@ -2,10 +2,7 @@ name: audit description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations. user-invocable: true -args: - - name: area - description: The feature or area to audit (optional) - required: false +argument-hint: "[area (feature, page, component...)]" --- Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address. diff --git a/.opencode/skills/bolder/SKILL.md b/.opencode/skills/bolder/SKILL.md index 192e3b97e..75500e712 100644 --- a/.opencode/skills/bolder/SKILL.md +++ b/.opencode/skills/bolder/SKILL.md @@ -2,10 +2,7 @@ name: bolder description: Amplify safe or boring designs to make them more visually interesting and stimulating. Increases impact while maintaining usability. user-invocable: true -args: - - name: target - description: The feature or component to make bolder (optional) - required: false +argument-hint: "[target]" --- Increase visual impact and personality in designs that are too safe, generic, or visually underwhelming, creating more engaging and memorable experiences. diff --git a/.opencode/skills/clarify/SKILL.md b/.opencode/skills/clarify/SKILL.md index e6a859286..f0f602fa4 100644 --- a/.opencode/skills/clarify/SKILL.md +++ b/.opencode/skills/clarify/SKILL.md @@ -2,10 +2,7 @@ name: clarify description: Improve unclear UX copy, error messages, microcopy, labels, and instructions. Makes interfaces easier to understand and use. user-invocable: true -args: - - name: target - description: The feature or component with unclear copy (optional) - required: false +argument-hint: "[target]" --- Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use. diff --git a/.opencode/skills/colorize/SKILL.md b/.opencode/skills/colorize/SKILL.md index b7c4aab90..c503c4b6c 100644 --- a/.opencode/skills/colorize/SKILL.md +++ b/.opencode/skills/colorize/SKILL.md @@ -2,10 +2,7 @@ name: colorize description: Add strategic color to features that are too monochromatic or lack visual interest. Makes interfaces more engaging and expressive. user-invocable: true -args: - - name: target - description: The feature or component to colorize (optional) - required: false +argument-hint: "[target]" --- Strategically introduce color to designs that are too monochromatic, gray, or lacking in visual warmth and personality. diff --git a/.opencode/skills/critique/SKILL.md b/.opencode/skills/critique/SKILL.md index bf735c842..62e2552fd 100644 --- a/.opencode/skills/critique/SKILL.md +++ b/.opencode/skills/critique/SKILL.md @@ -2,10 +2,7 @@ name: critique description: Evaluate design effectiveness from a UX perspective. Assesses visual hierarchy, information architecture, emotional resonance, and overall design quality with actionable feedback. user-invocable: true -args: - - name: area - description: The feature or area to critique (optional) - required: false +argument-hint: "[area (feature, page, component...)]" --- ## MANDATORY PREPARATION diff --git a/.opencode/skills/delight/SKILL.md b/.opencode/skills/delight/SKILL.md index 17af7899f..5aa6d85e5 100644 --- a/.opencode/skills/delight/SKILL.md +++ b/.opencode/skills/delight/SKILL.md @@ -2,10 +2,7 @@ name: delight description: Add moments of joy, personality, and unexpected touches that make interfaces memorable and enjoyable to use. Elevates functional to delightful. user-invocable: true -args: - - name: target - description: The feature or area to add delight to (optional) - required: false +argument-hint: "[target]" --- Identify opportunities to add moments of joy, personality, and unexpected polish that transform functional interfaces into delightful experiences. diff --git a/.opencode/skills/distill/SKILL.md b/.opencode/skills/distill/SKILL.md index 086b74345..0c476f598 100644 --- a/.opencode/skills/distill/SKILL.md +++ b/.opencode/skills/distill/SKILL.md @@ -2,10 +2,7 @@ name: distill description: Strip designs to their essence by removing unnecessary complexity. Great design is simple, powerful, and clean. user-invocable: true -args: - - name: target - description: The feature or component to distill (optional) - required: false +argument-hint: "[target]" --- Remove unnecessary complexity from designs, revealing the essential elements and creating clarity through ruthless simplification. diff --git a/.opencode/skills/extract/SKILL.md b/.opencode/skills/extract/SKILL.md index ca1b8cd81..98f62a04c 100644 --- a/.opencode/skills/extract/SKILL.md +++ b/.opencode/skills/extract/SKILL.md @@ -2,10 +2,7 @@ name: extract description: Extract and consolidate reusable components, design tokens, and patterns into your design system. Identifies opportunities for systematic reuse and enriches your component library. user-invocable: true -args: - - name: target - description: The feature, component, or area to extract from (optional) - required: false +argument-hint: "[target]" --- Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse. diff --git a/.opencode/skills/harden/SKILL.md b/.opencode/skills/harden/SKILL.md index 867842183..f901773a6 100644 --- a/.opencode/skills/harden/SKILL.md +++ b/.opencode/skills/harden/SKILL.md @@ -2,10 +2,7 @@ name: harden description: Improve interface resilience through better error handling, i18n support, text overflow handling, and edge case management. Makes interfaces robust and production-ready. user-invocable: true -args: - - name: target - description: The feature or area to harden (optional) - required: false +argument-hint: "[target]" --- Strengthen interfaces against edge cases, errors, internationalization issues, and real-world usage scenarios that break idealized designs. diff --git a/.opencode/skills/normalize/SKILL.md b/.opencode/skills/normalize/SKILL.md index 8a8f92356..f8af99752 100644 --- a/.opencode/skills/normalize/SKILL.md +++ b/.opencode/skills/normalize/SKILL.md @@ -2,10 +2,7 @@ name: normalize description: Normalize design to match your design system and ensure consistency user-invocable: true -args: - - name: feature - description: The page, route, or feature to normalize (optional) - required: false +argument-hint: "[feature (page, route, component...)]" --- Analyze and redesign the feature to perfectly match our design system standards, aesthetics, and established patterns. diff --git a/.opencode/skills/onboard/SKILL.md b/.opencode/skills/onboard/SKILL.md index 12d246dd8..10f3618b1 100644 --- a/.opencode/skills/onboard/SKILL.md +++ b/.opencode/skills/onboard/SKILL.md @@ -2,10 +2,7 @@ name: onboard description: Design or improve onboarding flows, empty states, and first-time user experiences. Helps users get started successfully and understand value quickly. user-invocable: true -args: - - name: target - description: The feature or area needing onboarding (optional) - required: false +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.opencode/skills/optimize/SKILL.md b/.opencode/skills/optimize/SKILL.md index 7240fa9aa..3f76667f6 100644 --- a/.opencode/skills/optimize/SKILL.md +++ b/.opencode/skills/optimize/SKILL.md @@ -2,10 +2,7 @@ name: optimize description: Improve interface performance across loading speed, rendering, animations, images, and bundle size. Makes experiences faster and smoother. user-invocable: true -args: - - name: target - description: The feature or area to optimize (optional) - required: false +argument-hint: "[target]" --- Identify and fix performance issues to create faster, smoother user experiences. diff --git a/.opencode/skills/overdrive/SKILL.md b/.opencode/skills/overdrive/SKILL.md index 96c945a1e..57a60bda3 100644 --- a/.opencode/skills/overdrive/SKILL.md +++ b/.opencode/skills/overdrive/SKILL.md @@ -2,10 +2,7 @@ name: overdrive description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?" user-invocable: true -args: - - name: target - description: The feature or area to push into overdrive (optional) - required: false +argument-hint: "[target]" --- Start your response with: diff --git a/.opencode/skills/polish/SKILL.md b/.opencode/skills/polish/SKILL.md index f8b8efbad..fe9e27687 100644 --- a/.opencode/skills/polish/SKILL.md +++ b/.opencode/skills/polish/SKILL.md @@ -2,10 +2,7 @@ name: polish description: Final quality pass before shipping. Fixes alignment, spacing, consistency, and detail issues that separate good from great. user-invocable: true -args: - - name: target - description: The feature or area to polish (optional) - required: false +argument-hint: "[target]" --- ## MANDATORY PREPARATION diff --git a/.opencode/skills/quieter/SKILL.md b/.opencode/skills/quieter/SKILL.md index 64566b1af..09bcdcbe5 100644 --- a/.opencode/skills/quieter/SKILL.md +++ b/.opencode/skills/quieter/SKILL.md @@ -2,10 +2,7 @@ name: quieter description: Tone down overly bold or visually aggressive designs. Reduces intensity while maintaining design quality and impact. user-invocable: true -args: - - name: target - description: The feature or component to make quieter (optional) - required: false +argument-hint: "[target]" --- Reduce visual intensity in designs that are too bold, aggressive, or overstimulating, creating a more refined and approachable aesthetic without losing effectiveness. diff --git a/.opencode/skills/typeset/SKILL.md b/.opencode/skills/typeset/SKILL.md index 246b99ef0..d331e2745 100644 --- a/.opencode/skills/typeset/SKILL.md +++ b/.opencode/skills/typeset/SKILL.md @@ -2,10 +2,7 @@ name: typeset description: Improve typography by fixing font choices, hierarchy, sizing, weight consistency, and readability. Makes text feel intentional and polished. user-invocable: true -args: - - name: target - description: The feature or component to improve typography for (optional) - required: false +argument-hint: "[target]" --- Assess and improve typography that feels generic, inconsistent, or poorly structured — turning default-looking text into intentional, well-crafted type. diff --git a/public/css/styles.css b/public/css/styles.css index 5e3aef6cb..6555d93b0 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1,3963 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -.split-container::before { - content: ''; - position: absolute; - inset: 0; - background-image: linear-gradient(var(--color-mist) 1px, transparent 1px), linear-gradient(90deg, var(--color-mist) 1px, transparent 1px); - background-size: 20px 20px; - opacity: 0.3; - pointer-events: none; -} -.split-container::after { - content: '← Drag →'; - position: absolute; - bottom: 12px; - left: 50%; - transform: translateX(-50%); - font-size: 0.625rem; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-ash); - background: var(--color-paper); - padding: 4px 12px; - border-radius: 4px; - opacity: 0.8; - transition: opacity 0.3s ease; - z-index: 10; -} -.split-container:hover::after { - opacity: 0; -} -.split-after .impeccable-card { - box-shadow: 0 10px 40px rgba(0,0,0,0.08); -} -@keyframes splitEntry { - from { - opacity: 0; - transform: translateX(-50%) skewX(-10deg) scaleY(0.8); - } - to { - opacity: 1; - transform: translateX(-50%) skewX(-10deg) scaleY(1); - } -} -.split-divider { - animation: splitEntry 0.6s var(--ease-out) 0.3s backwards; -} -.split-label-item { - transition: color var(--duration-fast) var(--ease-out); - cursor: default; -} -.split-label-item:hover { - color: var(--color-text); -} -.split-label-item[data-point="after"]:hover .split-label-dot--accent { - transform: scale(1.3); -} -.split-label-dot { - transition: transform var(--duration-fast) var(--ease-spring); -} -.split-badge { - position: absolute; - top: 10px; - font-size: 0.625rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - padding: 3px 8px; - border-radius: 3px; - z-index: 5; - pointer-events: none; -} -.split-badge--before { - left: 10px; - color: var(--color-ash); - background: var(--color-paper); - border: 1px solid var(--color-mist); -} -.split-badge--after { - right: 10px; - color: var(--color-paper); - background: var(--color-accent); -} -@media (hover: none) { - .split-container::after { - content: '← Swipe →'; - } -} -@media (max-width: 600px) { - .split-label { - font-size: 0.5625rem; - padding: 4px 10px; - } -} -.commands-section { - position: relative; - padding: var(--spacing-xl) 0; - background: var(--color-paper); -} -.commands-gallery { - display: block; -} -.commands-container { - display: grid; - grid-template-columns: 1fr 1.2fr; - gap: var(--spacing-2xl); - align-items: start; -} -@media (max-width: 900px) { - .commands-container { - grid-template-columns: 1fr; - } -} -.command-manual { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - padding-bottom: 20vh; -} -.command-category-header { - font-family: var(--font-display); - font-size: 0.875rem; - font-weight: 600; - color: var(--color-accent); - text-transform: uppercase; - letter-spacing: 0.1em; - padding: var(--spacing-lg) var(--spacing-lg) var(--spacing-sm); - margin-top: var(--spacing-md); - border-bottom: 1px solid var(--color-mist); -} -.command-category-header:first-child { - margin-top: 0; -} -.manual-entry { - position: relative; - padding: var(--spacing-lg); - padding-left: calc(var(--spacing-lg) + 16px); - border-left: 2px solid var(--color-mist); - transition: border-color 0.4s var(--ease-out), opacity 0.4s var(--ease-out), background 0.4s var(--ease-out), transform 0.4s var(--ease-out); - opacity: 0.4; - cursor: pointer; - transform: translateX(-16px); -} -.manual-entry:hover { - opacity: 0.7; -} -.manual-entry.active { - border-left-color: var(--color-accent); - opacity: 1; - transform: translateX(0); - background: linear-gradient(to right, var(--color-bg), transparent); -} -.manual-cmd-name { - font-family: var(--font-mono); - font-size: 1.5rem; - margin: 0 0 var(--spacing-sm); - color: var(--color-ink); - font-weight: 500; -} -.beta-badge { - font-family: var(--font-body); - font-size: 0.55rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-accent); - border: 1px solid var(--color-accent); - border-radius: 3px; - padding: 1px 5px; - vertical-align: middle; - margin-left: 6px; -} -.manual-cmd-desc { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - margin: 0; -} -.manual-cmd-rel { - font-size: 0.8125rem; - color: var(--color-ash); - margin-top: var(--spacing-sm); - display: flex; - align-items: center; - gap: 0.5ch; - flex-wrap: wrap; -} -.manual-cmd-rel .rel-icon { - color: var(--color-accent); - font-weight: 600; -} -.manual-cmd-rel code { - font-family: var(--font-mono); - font-size: 0.75rem; - background: var(--color-mist); - padding: 2px 6px; - border-radius: 3px; - color: var(--color-ink); -} -.glass-terminal-wrapper { - position: sticky; - top: var(--spacing-xl); - height: calc(100vh - var(--spacing-xl) * 2); - max-height: 800px; - min-height: 500px; -} -.terminal-stack { - position: relative; - height: 100%; - perspective: 1200px; -} -.terminal-stack-tabs { - position: absolute; - top: -31px; - right: 8px; - display: flex; - gap: 4px; - z-index: 10; -} -.terminal-stack-tab { - font-family: var(--font-mono); - font-size: 0.75rem; - padding: 5px 12px; - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-bottom: none; - border-radius: 6px 6px 0 0; - color: var(--color-ash); - cursor: pointer; - transition: all 0.2s ease; -} -.terminal-stack-tab:hover { - background: var(--color-paper); - color: var(--color-charcoal); -} -.terminal-stack-tab.active { - background: var(--color-paper); - color: var(--color-ink); - border-color: var(--color-mist); -} -.terminal-window { - position: absolute; - inset: 0; - transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease, filter 0.3s ease; - transform-origin: center bottom; -} -.terminal-window--demo { - z-index: 2; -} -.terminal-window--demo.is-back { - transform: translateY(16px) translateX(12px) scale(0.96); - opacity: 0.6; - filter: brightness(0.92); - pointer-events: none; - z-index: 1; -} -.terminal-window--source { - z-index: 1; - transform: translateY(16px) translateX(12px) scale(0.96); - opacity: 0.6; - filter: brightness(0.92); - pointer-events: none; -} -.terminal-window--source.is-front { - transform: translateY(0) translateX(0) scale(1); - opacity: 1; - filter: brightness(1); - pointer-events: auto; - z-index: 2; -} -.source-window { - background: var(--color-paper); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--color-mist); - border-radius: 8px; - box-shadow: 0 20px 60px -10px rgba(0,0,0,0.15); - height: 100%; - display: flex; - flex-direction: column; - overflow: hidden; -} -.source-header { - background: var(--color-cream); - padding: 12px 16px; - display: flex; - align-items: center; - gap: 8px; - border-bottom: 1px solid var(--color-mist); - flex-shrink: 0; -} -.source-title { - font-family: var(--font-mono); - font-size: 0.875rem; - color: var(--color-ink); - font-weight: 500; -} -.source-body { - flex: 1; - padding: var(--spacing-md); - font-family: var(--font-mono); - font-size: 0.75rem; - line-height: 1.5; - color: var(--color-charcoal); - overflow-y: auto; - overscroll-behavior: contain; - white-space: pre-wrap; - word-break: break-word; - background: var(--color-cream); -} -.source-loading { - color: var(--color-ash); - font-style: italic; -} -@media (max-width: 900px) { - .glass-terminal-wrapper { - display: none; - } -} -.glass-terminal { - background: var(--color-paper); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--color-mist); - border-radius: 8px; - box-shadow: 0 20px 60px -10px rgba(0,0,0,0.15); - height: 100%; - display: flex; - flex-direction: column; - overflow: hidden; -} -.terminal-header { - background: var(--color-cream); - padding: 12px 16px; - display: flex; - align-items: center; - gap: 8px; - border-bottom: 1px solid var(--color-mist); -} -.terminal-dot { - width: 10px; - height: 10px; - border-radius: 50%; -} -.terminal-dot.red { - background: #ff5f56; -} -.terminal-dot.yellow { - background: #ffbd2e; -} -.terminal-dot.green { - background: #27c93f; -} -.terminal-title { - margin-left: auto; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--color-ash); -} -.terminal-body { - flex: 1; - padding: var(--spacing-md); - font-family: var(--font-mono); - font-size: 0.9375rem; - color: var(--color-ink); - overflow-y: auto; - display: flex; - flex-direction: column; - min-height: 0; -} -.terminal-line { - margin-bottom: var(--spacing-sm); - display: flex; - gap: var(--spacing-sm); - line-height: 1.5; -} -.terminal-prompt { - color: var(--color-accent); - user-select: none; - font-weight: bold; -} -.terminal-cursor { - display: inline-block; - width: 8px; - height: 1.2em; - background: var(--color-accent); - vertical-align: middle; - animation: blink 1s step-end infinite; -} -.terminal-output { - color: var(--color-ash); - margin-bottom: var(--spacing-md); - white-space: pre-wrap; -} -@media (max-height: 800px) { - .terminal-output { - display: none; - } -} -.terminal-cmd { - color: var(--color-accent); - font-weight: 600; -} -.terminal-step { - color: var(--color-charcoal); -} -.terminal-done { - color: var(--color-success, #22c55e); - font-weight: 500; -} -.terminal-preview { - background: var(--color-paper); - margin: var(--spacing-sm) 0; - flex: 1; - min-height: 0; - overflow: hidden; - border-radius: 12px; -} -.terminal-cursor-line { - flex-shrink: 0; - margin-top: var(--spacing-sm) !important; -} -.terminal-preview .demo-split-comparison { - display: flex; - flex-direction: column; - height: 100%; -} -.terminal-preview .demo-split-comparison .split-container { - position: relative; - flex: 1; - min-height: 0; - overflow: hidden; - cursor: ew-resize; - user-select: none; - background: var(--color-cream); -} -.terminal-preview .demo-split-comparison .split-before, .terminal-preview .demo-split-comparison .split-after { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-md); -} -.terminal-preview .demo-split-comparison .split-before { - z-index: 1; - background: var(--color-cream); -} -.terminal-preview .demo-split-comparison .split-after { - z-index: 2; - background: var(--color-paper); - clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); -} -.terminal-preview .demo-split-comparison .split-content { - width: 100%; - max-width: 280px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} -.terminal-preview .demo-split-comparison .split-divider { - position: absolute; - top: 0; - bottom: 0; - left: 50%; - width: 2px; - background: var(--color-accent); - transform: translateX(-50%) skewX(-10deg); - pointer-events: none; - z-index: 3; - box-shadow: 0 0 12px rgba(0,0,0,0.1); -} -.terminal-preview .demo-split-comparison .split-label { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) skewX(10deg); - font-size: 0.5625rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-paper); - background: var(--color-accent); - padding: 4px 10px; - border-radius: 3px; - white-space: nowrap; -} -.terminal-preview .demo-split-comparison .demo-caption { - flex-shrink: 0; - font-size: 0.75rem; - color: var(--color-ash); - text-align: center; - padding: var(--spacing-sm) var(--spacing-md); -} -@keyframes blink { - 50% { - opacity: 0; - } -} -.casestudies-section { - position: relative; - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.transformations-tabbed { - margin-top: var(--spacing-xl); -} -.transformation-tabs { - display: flex; - gap: var(--spacing-xs); - border-bottom: 1px solid var(--color-mist); - margin-bottom: var(--spacing-lg); -} -.transformation-tab { - font-family: var(--font-display); - font-size: 0.9375rem; - font-weight: 500; - color: var(--color-ash); - background: none; - border: none; - padding: var(--spacing-sm) var(--spacing-md); - cursor: pointer; - position: relative; - transition: color 0.2s ease; -} -.transformation-tab:hover { - color: var(--color-charcoal); -} -.transformation-tab.active { - color: var(--color-ink); -} -.transformation-tab.active::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - right: 0; - height: 2px; - background: var(--color-accent); -} -.transformation-panels { - position: relative; -} -.transformation-panel { - display: none; - flex-direction: column; - gap: var(--spacing-lg); - animation: fadeInPanel 0.3s ease; -} -.transformation-panel.active { - display: flex; -} -@keyframes fadeInPanel { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.transformation-images { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.transformation-before, .transformation-after { - flex: 1; - margin: 0; -} -.transformation-before img, .transformation-after img, .transformation-placeholder { - width: 100%; - aspect-ratio: 16 / 10; - object-fit: cover; - border-radius: 8px; - border: 1px solid var(--color-mist); - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} -.transformation-before img:hover, .transformation-after img:hover, .transformation-placeholder:hover { - transform: scale(1.02); - box-shadow: 0 8px 24px -4px rgba(0,0,0,0.15); -} -.transformation-placeholder { - background: linear-gradient(135deg, var(--color-mist) 0%, var(--color-cream) 100%); - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ash); - font-size: 0.8125rem; - font-style: italic; -} -.transformation-before figcaption, .transformation-after figcaption { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ash); - margin-top: var(--spacing-xs); - text-align: center; -} -.transformation-arrow { - font-size: 1.5rem; - color: var(--color-accent); - font-weight: 300; - flex-shrink: 0; -} -.transformation-info { - max-width: 600px; -} -.transformation-title { - font-family: var(--font-display); - font-size: 1.25rem; - font-weight: 600; - color: var(--color-ink); - margin: 0 0 var(--spacing-xs); -} -.transformation-desc { - font-size: 0.9375rem; - color: var(--color-charcoal); - line-height: 1.6; - margin: 0 0 var(--spacing-sm); -} -.transformation-commands { - display: flex; - flex-wrap: wrap; - gap: 6px; -} -.transformation-command { - font-family: var(--font-mono); - font-size: 0.75rem; - background: var(--color-mist); - color: var(--color-charcoal); - padding: 4px 10px; - border-radius: 4px; -} -.lightbox { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.9); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; -} -.lightbox.active { - opacity: 1; - visibility: visible; -} -.lightbox-close { - position: absolute; - top: 20px; - right: 24px; - background: none; - border: none; - color: white; - font-size: 2.5rem; - cursor: pointer; - opacity: 0.7; - transition: opacity 0.2s ease; - line-height: 1; -} -.lightbox-close:hover { - opacity: 1; -} -.lightbox-image { - max-width: 90vw; - max-height: 85vh; - object-fit: contain; - border-radius: 8px; - box-shadow: 0 20px 60px rgba(0,0,0,0.5); -} -@media (max-width: 768px) { - .transformation-images { - flex-direction: column; - } - .transformation-arrow { - transform: rotate(90deg); - } - .transformation-before, .transformation-after { - width: 100%; - } -} -.hero-version-link { - font-size: 0.8125rem; - color: var(--color-ash); - margin-top: var(--spacing-sm); -} -.hero-version-link a { - color: var(--color-ash); - text-decoration: none; - border-bottom: 1px solid transparent; - transition: color 0.2s ease, border-color 0.2s ease; -} -.hero-version-link a:hover { - color: var(--color-accent); - border-bottom-color: var(--color-accent); -} -.changelog-section { - position: relative; - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-mist); -} -.changelog-list { - display: flex; - flex-direction: column; - gap: 0; -} -.changelog-entry { - padding: var(--spacing-md) 0; - border-bottom: 1px solid var(--color-mist); -} -.changelog-entry:first-child { - border-top: 1px solid var(--color-mist); -} -.changelog-version-header { - display: flex; - align-items: baseline; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-sm); -} -.changelog-version { - font-family: var(--font-mono); - font-size: 1.125rem; - font-weight: 600; - color: var(--color-ink); -} -.changelog-date { - font-size: 0.8125rem; - color: var(--color-ash); -} -.changelog-items { - margin: 0; - padding-left: var(--spacing-md); - color: var(--color-charcoal); - line-height: 1.7; -} -.changelog-items li { - margin-bottom: var(--spacing-xs); -} -.changelog-items code { - font-family: var(--font-mono); - font-size: 0.875em; - background: var(--color-mist); - padding: 2px 6px; - border-radius: 3px; - color: var(--color-ink); -} -.faq-section { - position: relative; - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-mist); -} -.faq-list { - display: flex; - flex-direction: column; - gap: 0; -} -.faq-item { - border-bottom: 1px solid var(--color-mist); -} -.faq-item:first-child { - border-top: 1px solid var(--color-mist); -} -.faq-question { - font-family: var(--font-display); - font-size: 1.125rem; - font-weight: 500; - color: var(--color-ink); - padding: var(--spacing-md) 0; - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - justify-content: space-between; - transition: color 0.2s ease; -} -.faq-question::-webkit-details-marker { - display: none; -} -.faq-question::after { - content: '+'; - font-family: var(--font-body); - font-size: 1.5rem; - font-weight: 300; - color: var(--color-accent); - transition: transform 0.3s var(--ease-out); -} -.faq-item[open] .faq-question::after { - transform: rotate(45deg); -} -.faq-question:hover { - color: var(--color-accent); -} -.faq-answer { - padding: 0 0 var(--spacing-md); - color: var(--color-charcoal); - line-height: 1.7; - animation: faqFadeIn 0.3s var(--ease-out); -} -.faq-answer p { - margin: 0 0 var(--spacing-sm); -} -.faq-answer p:last-child { - margin-bottom: 0; -} -.faq-answer ul { - margin: var(--spacing-sm) 0; - padding-left: var(--spacing-md); -} -.faq-answer li { - margin-bottom: var(--spacing-xs); -} -.faq-answer code { - font-family: var(--font-mono); - font-size: 0.875em; - background: var(--color-mist); - padding: 2px 6px; - border-radius: 3px; - color: var(--color-ink); -} -.faq-answer a { - color: var(--color-accent); - text-decoration: none; - border-bottom: 1px solid transparent; - transition: border-color 0.2s ease; -} -.faq-answer a:hover { - border-bottom-color: var(--color-accent); -} -@keyframes faqFadeIn { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.skills-section { - position: relative; - padding: var(--spacing-xl) 0; - overflow: hidden; - background: var(--color-bg); -} -.skills-gallery { - display: block; - position: relative; -} -.gallery-track { - display: flex; - gap: var(--spacing-lg); - overflow-x: auto; - scroll-snap-type: x mandatory; - padding: var(--spacing-md) var(--spacing-lg) var(--spacing-xl); - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - cursor: grab; -} -.gallery-track:active { - cursor: grabbing; -} -.gallery-track::-webkit-scrollbar { - display: none; -} -.gallery-frame { - flex: 0 0 80vw; - max-width: 1100px; - scroll-snap-align: center; - position: relative; - background: var(--color-paper); - border: 1px solid var(--color-mist); - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 20px 50px -10px rgba(0, 0, 0, 0.1); - border-radius: 2px; - overflow: hidden; - opacity: 0.4; - transform: scale(0.95); - transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out), box-shadow 0.6s var(--ease-out); -} -.gallery-frame.active { - opacity: 1; - transform: scale(1); - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 40px 100px -20px rgba(0, 0, 0, 0.2); - border-color: var(--color-charcoal); - border-width: 1px; -} -.gallery-content { - display: grid; - grid-template-columns: 1.2fr 1fr; - height: 600px; -} -@media (max-width: 900px) { - .gallery-frame { - flex: 0 0 90vw; - } - .gallery-content { - grid-template-columns: 1fr; - height: auto; - min-height: 600px; - } -} -.gallery-visual { - background: var(--color-cream); - border-right: 1px solid var(--color-mist); - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-lg); -} -.gallery-info { - padding: var(--spacing-xl); - display: flex; - flex-direction: column; - overflow-y: auto; -} -.gallery-header { - margin-bottom: var(--spacing-lg); -} -.gallery-title { - font-family: var(--font-display); - font-size: 2.5rem; - font-style: italic; - margin: 0 0 var(--spacing-xs); - color: var(--color-ink); -} -.gallery-meta { - font-family: var(--font-mono); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.gallery-desc { - font-size: 1.125rem; - line-height: 1.6; - color: var(--color-charcoal); - margin-bottom: var(--spacing-xl); - max-width: 45ch; -} -.gallery-tags { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - margin-top: auto; -} -.gallery-tag { - padding: 6px 12px; - border: 1px solid var(--color-mist); - border-radius: 4px; - font-size: 0.8125rem; - color: var(--color-ash); -} -.gallery-map { - display: flex; - justify-content: center; - gap: 8px; - margin-top: var(--spacing-lg); -} -.gallery-dot { - width: 40px; - height: 2px; - background: var(--color-mist); - cursor: pointer; - transition: all 0.3s ease; - position: relative; - border: none; - padding: 0; - font: inherit; -} -.gallery-dot::after { - content: ''; - position: absolute; - top: -10px; - bottom: -10px; - left: 0; - right: 0; -} -.gallery-dot:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 4px; - border-radius: 1px; -} -.gallery-dot.active { - background: var(--color-accent); - height: 4px; -} -.demo-tabbed-container { - display: flex; - flex-direction: column; -} -.demo-tabs { - display: flex; - gap: 0; - margin-bottom: 0; - justify-content: center; - background: var(--color-paper); - border-bottom: 1px solid var(--color-mist); -} -.demo-tab { - padding: var(--spacing-sm) var(--spacing-lg); - background: transparent; - border: none; - border-bottom: 2px solid transparent; - font-family: var(--font-mono); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.05em; - text-transform: uppercase; - color: var(--color-ash); - cursor: pointer; - transition: all var(--duration-fast) var(--ease-out); -} -.demo-tab:hover { - color: var(--color-text); - background: var(--color-cream); -} -.demo-tab.active { - color: var(--color-accent); - border-bottom-color: var(--color-accent); - background: var(--color-accent-dim); -} -.demo-panels { - flex: 1; -} -.demo-panel { - display: none; -} -.demo-panel.active { - display: block; - animation: fadeSlideIn 0.3s var(--ease-out); -} -@keyframes fadeSlideIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.demo-container { - background: var(--color-paper); - border: none; - border-radius: 0; - overflow: hidden; -} -.demo-header { - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-sm) var(--spacing-md); - background: var(--color-paper); - border-bottom: 1px solid var(--color-mist); - min-height: 48px; -} -.demo-toggle { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.demo-toggle-label { - font-family: var(--font-mono); - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - transition: color var(--duration-fast) var(--ease-out); - cursor: pointer; -} -.demo-toggle-label:hover { - color: var(--color-text); -} -.demo-toggle-label.active { - color: var(--color-accent); -} -.demo-toggle-switch { - position: relative; - width: 44px; - height: 24px; - background: var(--color-mist); - border-radius: 12px; - cursor: pointer; - transition: background var(--duration-fast) var(--ease-out); - border: 1px solid transparent; - padding: 0; - font: inherit; -} -.demo-toggle-switch:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.demo-toggle-switch:hover { - border-color: var(--color-ash); -} -.demo-toggle-switch::after { - content: ''; - position: absolute; - top: 3px; - left: 3px; - width: 16px; - height: 16px; - background: var(--color-paper); - border-radius: 50%; - box-shadow: 0 1px 4px rgba(0,0,0,0.15); - transition: transform var(--duration-base) var(--ease-spring); -} -.demo-toggle-switch.active { - background: var(--color-accent); -} -.demo-toggle-switch.active::after { - transform: translateX(20px); -} -.demo-viewport { - padding: var(--spacing-xl); - display: flex; - align-items: center; - justify-content: center; - min-height: 280px; - background: var(--color-cream); - transition: background var(--duration-base) var(--ease-out); -} -.demo-viewport[data-state="after"] { - background: var(--color-paper); -} -.demo-caption { - padding: var(--spacing-sm) var(--spacing-md); - font-family: var(--font-mono); - font-size: 0.6875rem; - letter-spacing: 0.03em; - color: var(--color-ash); - background: var(--color-paper); - text-align: center; -} -.uxw-demo { - width: 100%; - max-width: 320px; - padding: var(--spacing-lg); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; - text-align: center; -} -.uxw-error-icon { - font-size: 2rem; - margin-bottom: var(--spacing-sm); -} -.uxw-error-title { - font-weight: 600; - color: #c00; - margin-bottom: var(--spacing-xs); -} -.uxw-error-text { - font-size: 0.875rem; - color: var(--color-ash); -} -.uxw-error-action { - margin-top: var(--spacing-sm); - font-size: 0.875rem; - color: var(--color-accent); - cursor: pointer; - text-decoration: underline; -} -.uxw-error-after .uxw-error-icon { - color: var(--color-accent); -} -.uxw-error-after .uxw-error-title { - color: var(--color-text); -} -.uxw-error-after .uxw-error-text { - color: var(--color-charcoal); -} -.uxw-button-context { - font-size: 0.875rem; - color: var(--color-charcoal); - margin-bottom: var(--spacing-md); - font-weight: 500; -} -.uxw-button-row { - display: flex; - gap: var(--spacing-sm); - justify-content: center; -} -.uxw-btn { - padding: var(--spacing-xs) var(--spacing-md); - border-radius: 4px; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - border: none; -} -.uxw-btn-primary { - background: var(--color-text); - color: var(--color-paper); -} -.uxw-btn-secondary { - background: transparent; - color: var(--color-ash); - border: 1px solid var(--color-mist); -} -.uxw-btn-danger { - background: #c00; - color: white; -} -.uxw-empty-icon { - font-size: 2.5rem; - margin-bottom: var(--spacing-sm); - opacity: 0.4; -} -.uxw-empty-title { - font-weight: 500; - color: var(--color-ash); -} -.uxw-empty-text { - font-size: 0.875rem; - color: var(--color-charcoal); - margin-top: var(--spacing-xs); -} -.uxw-empty-action { - margin-top: var(--spacing-md); -} -.uxw-empty-after .uxw-empty-icon { - opacity: 1; -} -.uxw-empty-after .uxw-empty-title { - color: var(--color-text); -} -.spatial-demo { - width: 100%; - max-width: 340px; - padding: var(--spacing-md); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; -} -.spatial-grid-before { - display: flex; - flex-wrap: wrap; - gap: 6px; -} -.spatial-grid-after { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-sm); -} -.spatial-card-item { - padding: var(--spacing-sm); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 4px; - font-size: 0.8125rem; - color: var(--color-charcoal); - text-align: center; -} -.spatial-grid-after .spatial-card-item { - width: auto !important; -} -.spatial-hierarchy-before .spatial-h-title, .spatial-hierarchy-before .spatial-h-subtitle, .spatial-hierarchy-before .spatial-h-cta, .spatial-hierarchy-before .spatial-h-link { - font-size: 0.9375rem; - margin-bottom: var(--spacing-xs); - color: var(--color-charcoal); -} -.spatial-hierarchy-after .spatial-h-title { - font-family: var(--font-display); - font-size: 1.75rem; - font-weight: 300; - font-style: italic; - margin-bottom: var(--spacing-xs); - color: var(--color-text); -} -.spatial-hierarchy-after .spatial-h-subtitle { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); - margin-bottom: var(--spacing-md); -} -.spatial-hierarchy-after .spatial-h-cta { - display: inline-block; - padding: var(--spacing-sm) var(--spacing-lg); - background: var(--color-text); - color: var(--color-paper); - font-size: 0.875rem; - font-weight: 500; - border-radius: 4px; - margin-bottom: var(--spacing-sm); -} -.spatial-hierarchy-after .spatial-h-link { - font-size: 0.75rem; - color: var(--color-ash); -} -.spatial-whitespace-before { - padding: var(--spacing-xs) !important; -} -.spatial-whitespace-before .spatial-ws-title { - font-size: 1rem; - font-weight: 600; - margin-bottom: 2px; -} -.spatial-whitespace-before .spatial-ws-price { - font-size: 0.875rem; - color: var(--color-ash); - margin-bottom: 4px; -} -.spatial-whitespace-before .spatial-ws-features { - font-size: 0.75rem; - color: var(--color-ash); - margin-bottom: 6px; -} -.spatial-whitespace-before .spatial-ws-btn { - width: 100%; - padding: 6px; - font-size: 0.75rem; - background: var(--color-text); - color: var(--color-paper); - border: none; - border-radius: 3px; - cursor: pointer; -} -.spatial-whitespace-after { - padding: var(--spacing-lg) !important; -} -.spatial-whitespace-after .spatial-ws-title { - font-family: var(--font-display); - font-size: 1.5rem; - font-weight: 400; - margin-bottom: var(--spacing-sm); -} -.spatial-whitespace-after .spatial-ws-price { - font-size: 1.25rem; - font-weight: 600; - color: var(--color-text); - margin-bottom: var(--spacing-sm); -} -.spatial-whitespace-after .spatial-ws-features { - font-size: 0.8125rem; - color: var(--color-ash); - margin-bottom: var(--spacing-lg); - line-height: 1.6; -} -.spatial-whitespace-after .spatial-ws-btn { - width: 100%; - padding: var(--spacing-sm); - font-size: 0.875rem; - background: var(--color-text); - color: var(--color-paper); - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; -} -.motion-demo { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-sm); - width: 100%; - max-width: 280px; -} -.motion-stagger-demo { - align-items: stretch; -} -.motion-list-item { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-md); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 4px; - font-size: 0.875rem; - color: var(--color-charcoal); -} -.motion-dot { - width: 8px; - height: 8px; - background: var(--color-accent); - border-radius: 50%; -} -.demo-viewport[data-state="after"] .motion-list-item { - opacity: 0; - transform: translateY(12px); - animation: staggerIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(1) { - animation-delay: 0s; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(2) { - animation-delay: 0.05s; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(3) { - animation-delay: 0.1s; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(4) { - animation-delay: 0.15s; -} -@keyframes staggerIn { - to { - opacity: 1; - transform: translateY(0); - } -} -.motion-btn { - padding: 12px 24px; - font-size: 0.9375rem; - font-weight: 500; - border: none; - border-radius: 4px; - cursor: pointer; -} -.motion-btn-before { - background: var(--color-charcoal); - color: var(--color-paper); -} -.motion-btn-after { - background: var(--color-text); - color: var(--color-paper); - transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; -} -.motion-btn-after:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.15); -} -.motion-btn-after:active { - transform: translateY(0) scale(0.98); -} -.motion-card { - padding: var(--spacing-md); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 6px; - text-align: center; - min-width: 140px; -} -.motion-card-icon { - font-size: 1.5rem; - margin-bottom: var(--spacing-xs); -} -.motion-card-text { - font-size: 0.8125rem; - color: var(--color-charcoal); -} -.motion-card-after { - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} -.demo-viewport[data-state="after"] .motion-card-after { - background: var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklch, var(--color-accent) 10%, var(--color-paper)); - } - border-color: var(--color-accent); -} -.demo-viewport[data-state="after"] .motion-card-after .motion-card-icon { - animation: checkPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); -} -@keyframes checkPop { - 50% { - transform: scale(1.3); - } -} -.typo-demo { - width: 100%; - max-width: 320px; - text-align: left; -} -.typo-pairing-before { - font-family: 'Inter', system-ui, sans-serif; -} -.typo-pairing-before .typo-heading { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: var(--spacing-xs); -} -.typo-pairing-before .typo-body { - font-size: 0.9375rem; - line-height: 1.5; - color: var(--color-ash); -} -.typo-pairing-after .typo-heading { - font-family: var(--font-display); - font-size: 2rem; - font-weight: 300; - font-style: italic; - letter-spacing: -0.02em; - margin-bottom: var(--spacing-sm); - color: var(--color-text); -} -.typo-pairing-after .typo-body { - font-family: var(--font-body); - font-size: 0.9375rem; - line-height: 1.7; - color: var(--color-charcoal); -} -.typo-hierarchy-before .typo-h1 { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 4px; -} -.typo-hierarchy-before .typo-meta { - font-size: 0.9375rem; - color: var(--color-ash); - margin-bottom: var(--spacing-xs); -} -.typo-hierarchy-before .typo-p { - font-size: 0.875rem; - line-height: 1.5; - color: var(--color-charcoal); -} -.typo-hierarchy-after .typo-h1 { - font-family: var(--font-display); - font-size: 2.25rem; - font-weight: 300; - letter-spacing: -0.03em; - margin-bottom: 2px; - line-height: 1.1; -} -.typo-hierarchy-after .typo-meta { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--color-accent); - margin-bottom: var(--spacing-md); -} -.typo-hierarchy-after .typo-p { - font-size: 0.9375rem; - line-height: 1.7; - color: var(--color-ash); -} -.int-demo { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - width: 100%; - max-width: 280px; -} -.int-states-demo { - gap: var(--spacing-lg); -} -.int-state-row { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.int-state-label { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - width: 40px; -} -.int-btn { - flex: 1; - padding: var(--spacing-sm) var(--spacing-md); - font-size: 0.875rem; - font-weight: 500; - border-radius: 4px; - cursor: pointer; -} -.int-btn-poor { - background: var(--color-charcoal); - color: var(--color-paper); - border: none; -} -.int-btn-good { - background: var(--color-text); - color: var(--color-paper); - border: 2px solid transparent; - transition: all 0.15s ease; -} -.int-btn-good:hover { - background: var(--color-charcoal); -} -.int-btn-good:focus { - outline: none; - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-accent) 25%, transparent); - } -} -.int-btn-good:active { - transform: scale(0.98); -} -.int-aff-item { - padding: var(--spacing-sm) var(--spacing-md); - border-radius: 4px; - font-size: 0.875rem; - cursor: pointer; -} -.int-aff-poor { - color: var(--color-charcoal); -} -.int-aff-good { - color: var(--color-accent); - text-decoration: underline; - text-underline-offset: 2px; -} -.int-aff-good::after { - content: ' →'; -} -.int-affordance-after .int-aff-item { - background: var(--color-bg); - border: 1px solid var(--color-mist); - color: var(--color-accent); - text-decoration: underline; - text-underline-offset: 2px; - transition: background 0.15s ease; -} -.int-affordance-after .int-aff-item:hover { - background: var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklch, var(--color-accent) 5%, var(--color-paper)); - } -} -.int-affordance-after .int-aff-item::after { - content: ' →'; -} -.int-feedback-before, .int-feedback-after { - display: flex; - align-items: center; - gap: var(--spacing-md); - flex-direction: row; -} -.int-fb-btn { - width: 48px; - height: 48px; - border-radius: 50%; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} -.int-fb-btn svg { - width: 22px; - height: 22px; -} -.int-fb-silent { - background: var(--color-mist); - color: var(--color-ash); -} -.int-fb-active { - background: var(--color-charcoal); - color: var(--color-paper); - transition: all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); -} -.int-fb-active:hover { - transform: scale(1.1); -} -.int-fb-active:active { - transform: scale(0.95); -} -.int-fb-active.liked { - background: var(--color-accent); - animation: heartPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); -} -@keyframes heartPop { - 50% { - transform: scale(1.25); - } -} -.int-fb-label { - font-size: 0.875rem; - color: var(--color-charcoal); -} -.color-demo { - width: 100%; - max-width: 300px; -} -.color-palette-before, .color-palette-after { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - padding: var(--spacing-md); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; -} -.color-swatch { - width: 40px; - height: 40px; - border-radius: 4px; - transition: background 0.2s ease; -} -.color-card { - width: 100%; - margin-top: var(--spacing-sm); - padding: var(--spacing-sm); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 4px; - display: flex; - flex-direction: column; - gap: 4px; -} -.color-card span { - font-size: 0.8125rem; - font-weight: 500; - transition: color 0.2s ease; -} -.color-card button { - padding: 6px; - border: none; - border-radius: 3px; - font-size: 0.75rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} -.color-palette-before .swatch-1 { - background: #ff6b6b; -} -.color-palette-before .swatch-2 { - background: #4ecdc4; -} -.color-palette-before .swatch-3 { - background: #ffe66d; -} -.color-palette-before .swatch-4 { - background: #95e1d3; -} -.color-palette-before .swatch-5 { - background: #f38181; -} -.color-palette-before .card-title { - color: #ff6b6b; -} -.color-palette-before .card-subtitle { - color: #4ecdc4; -} -.color-palette-before .card-btn { - background: #ffe66d; - color: #333; -} -.color-palette-after .swatch-1 { - background: var(--color-text); -} -.color-palette-after .swatch-2 { - background: var(--color-charcoal); -} -.color-palette-after .swatch-3 { - background: var(--color-ash); -} -.color-palette-after .swatch-4 { - background: var(--color-mist); -} -.color-palette-after .swatch-5 { - background: var(--color-accent); -} -.color-palette-after .card-title { - color: var(--color-text); -} -.color-palette-after .card-subtitle { - color: var(--color-ash); -} -.color-palette-after .card-btn { - background: var(--color-accent); - color: var(--color-paper); -} -.color-accent-card { - padding: var(--spacing-md); - border-radius: 6px; -} -.color-accent-before .color-accent-card { - background: #f5f5f5; - border: 1px solid #e0e0e0; -} -.color-accent-before .color-accent-title { - font-weight: 600; - color: #333; - margin-bottom: 4px; -} -.color-accent-before .color-accent-text { - font-size: 0.8125rem; - color: #666; - margin-bottom: var(--spacing-sm); -} -.color-accent-before .color-accent-btn { - width: 100%; - padding: var(--spacing-xs); - background: #333; - color: white; - border: none; - border-radius: 4px; - font-size: 0.8125rem; - cursor: pointer; -} -.color-accent-after .color-accent-card { - background: var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklch, var(--color-accent) 8%, var(--color-paper)); - } - border: 1px solid var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - border: 1px solid color-mix(in oklch, var(--color-accent) 20%, var(--color-paper)); - } -} -.color-accent-after .color-accent-title { - font-weight: 600; - color: var(--color-text); - margin-bottom: 4px; -} -.color-accent-after .color-accent-text { - font-size: 0.8125rem; - color: var(--color-ash); - margin-bottom: var(--spacing-sm); -} -.color-accent-after .color-accent-btn { - width: 100%; - padding: var(--spacing-xs); - background: var(--color-accent); - color: var(--color-paper); - border: none; - border-radius: 4px; - font-size: 0.8125rem; - font-weight: 500; - cursor: pointer; -} -.color-contrast-static { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} -.contrast-example { - padding: var(--spacing-md); - border-radius: 6px; - text-align: center; -} -.contrast-fail { - background: #f0f0f0; - color: #a0a0a0; -} -.contrast-pass { - background: var(--color-charcoal); - color: var(--color-paper); -} -.contrast-badge { - display: inline-block; - font-size: 0.5625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - padding: 2px 6px; - border-radius: 2px; - margin-bottom: 4px; -} -.contrast-fail .contrast-badge { - background: #ddd; -} -.contrast-pass .contrast-badge { - background: var(--color-accent); - color: var(--color-paper); -} -.contrast-text { - font-size: 1rem; - font-weight: 500; - margin-bottom: 2px; -} -.contrast-ratio { - font-size: 0.6875rem; - opacity: 0.7; -} -.resp-demo { - width: 100%; - max-width: 340px; -} -.resp-touch-demo { - display: flex; - flex-direction: column; - gap: var(--spacing-lg); -} -.resp-touch-row { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.resp-label { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - width: 70px; -} -.resp-touch-targets { - display: flex; - gap: 4px; -} -.resp-touch-targets button { - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; -} -.resp-touch-bad button { - width: 24px; - height: 24px; - font-size: 0.75rem; - background: var(--color-mist); - color: var(--color-ash); -} -.resp-touch-good button { - width: 44px; - height: 44px; - font-size: 1rem; - background: var(--color-text); - color: var(--color-paper); -} -.resp-fluid-demo { - padding: var(--spacing-md); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 6px; -} -.resp-fluid-container { - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} -.resp-fluid-fixed, .resp-fluid-adaptive { - font-size: 0.75rem; - color: var(--color-ash); -} -.resp-fluid-fixed span, .resp-fluid-adaptive span { - display: block; - margin-bottom: 4px; -} -.resp-fluid-bar { - height: 24px; - background: var(--color-mist); - border-radius: 4px; -} -.resp-fluid-adaptive .resp-fluid-bar { - background: var(--color-accent); -} -.resp-adapt-demo { - display: flex; - gap: var(--spacing-sm); - align-items: flex-end; -} -.resp-device { - text-align: center; -} -.resp-device > span { - display: block; - margin-top: 4px; - font-size: 0.625rem; - color: var(--color-ash); - text-transform: uppercase; - letter-spacing: 0.08em; -} -.resp-device-screen { - background: var(--color-paper); - border: 2px solid var(--color-mist); - border-radius: 4px; - padding: 4px; - display: flex; - flex-direction: column; - gap: 3px; -} -.resp-device-mobile .resp-device-screen { - width: 50px; - height: 80px; -} -.resp-device-tablet .resp-device-screen { - width: 80px; - height: 60px; -} -.resp-device-desktop .resp-device-screen { - width: 120px; - height: 70px; -} -.resp-block { - background: var(--color-mist); - border-radius: 2px; -} -.resp-block-row { - display: flex; - gap: 3px; - flex: 1; -} -.resp-header { - height: 16px; - background: var(--color-charcoal); -} -.resp-sidebar { - width: 30%; - background: var(--color-charcoal); -} -.resp-content { - flex: 1; -} -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} -*, *::before, *::after { - box-sizing: border-box; -} -* { - margin: 0; -} -img, picture, video, canvas, svg { - display: block; - max-width: 100%; -} -button, input, textarea, select { - font: inherit; -} -:root { - --font-display: 'Cormorant Garamond', Georgia, serif; - --font-body: 'Instrument Sans', system-ui, sans-serif; - --font-mono: 'Space Grotesk', monospace; - --spacing-xs: 8px; - --spacing-sm: 16px; - --spacing-md: 24px; - --spacing-lg: 32px; - --spacing-xl: 48px; - --spacing-2xl: 80px; - --spacing-3xl: 120px; - --width-max: 1400px; - --width-content: 900px; - --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); - --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); - --duration-fast: 0.15s; - --duration-base: 0.3s; - --duration-slow: 0.6s; - --duration-slower: 0.8s; - --duration-slowest: 1.2s; - --color-ink: oklch(10% 0 0); - --color-text: oklch(10% 0 0); - --color-paper: oklch(98% 0 0); - --color-cream: oklch(96% 0.005 350); - --color-charcoal: oklch(25% 0 0); - --color-ash: oklch(55% 0 0); - --color-mist: oklch(92% 0 0); - --color-bg: oklch(96% 0.005 350); - --color-accent: oklch(60% 0.25 350); - --color-accent-hover: oklch(52% 0.25 350); - --color-accent-dim: oklch(60% 0.25 350 / 0.15); - --color-accent-soft: oklch(60% 0.25 350 / 0.25); - --cat-diagnostic-bg: #fdf4ff; - --cat-diagnostic-border: #d946ef; - --cat-diagnostic-text: #a21caf; - --cat-quality-bg: #f0fdf4; - --cat-quality-border: #22c55e; - --cat-quality-text: #15803d; - --cat-intensity-bg: #fffbeb; - --cat-intensity-border: #f59e0b; - --cat-intensity-text: #b45309; - --cat-adaptation-bg: #eff6ff; - --cat-adaptation-border: #3b82f6; - --cat-adaptation-text: #1d4ed8; - --cat-enhancement-bg: #fdf2f8; - --cat-enhancement-border: #ec4899; - --cat-enhancement-text: #be185d; - --cat-system-bg: #f5f5f4; - --cat-system-border: #78716c; - --cat-system-text: #44403c; -} -.skip-link { - position: absolute; - top: -100%; - left: 50%; - transform: translateX(-50%); - z-index: 10000; - padding: var(--spacing-sm) var(--spacing-lg); - background: var(--color-ink); - color: var(--color-paper); - font-weight: 600; - text-decoration: none; - border-radius: 0 0 8px 8px; - transition: top 0.2s ease; -} -.skip-link:focus { - top: 0; - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; - overflow-x: clip; -} -body { - font-family: var(--font-body); - font-size: 16px; - line-height: 1.625; - color: var(--color-text); - background: var(--color-paper); - overflow-x: clip; - min-height: 100vh; - min-height: 100dvh; -} -h1, h2, h3, h4, h5, h6 { - font-family: var(--font-display); - font-weight: 400; - line-height: 1.1; - letter-spacing: -0.02em; - color: var(--color-ink); -} -a { - color: var(--color-accent); - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 2px; - transition: color var(--duration-fast) var(--ease-out), text-decoration-color var(--duration-fast) var(--ease-out); -} -a:hover { - color: var(--color-accent-hover); - text-decoration-thickness: 2px; -} -.btn, .footer-logo, [class*="nav-item"] { - text-decoration: none; -} -strong { - font-weight: 600; - color: var(--color-ink); -} -code { - font-family: var(--font-mono); - font-size: 0.9em; - padding: 0.15em 0.4em; - background: var(--color-accent-dim); - color: var(--color-accent); - border-radius: 4px; -} -::selection { - background: var(--color-accent-soft); - color: var(--color-ink); -} -.grain-overlay { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 9999; - opacity: 0.03; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); - background-repeat: repeat; -} -.site-content { - max-width: var(--width-max); - margin: 0 auto; - padding: 0 var(--spacing-lg); -} -@media (max-width: 768px) { - .site-content { - padding: 0 var(--spacing-md); - } -} -.section-header { - margin-bottom: var(--spacing-lg); - position: relative; -} -.section-number { - display: block; - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 500; - letter-spacing: 0.05em; - color: var(--color-ash); - margin-bottom: var(--spacing-xs); - text-transform: uppercase; -} -.section-title { - font-size: clamp(1.75rem, 4vw, 2.5rem); - font-weight: 400; - line-height: 1.2; - margin: 0; -} -.section-subtitle { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - margin-top: var(--spacing-sm); - max-width: 55ch; -} -.cheatsheet-link { - color: var(--color-accent); - text-decoration: none; - font-size: 0.875rem; - margin-left: 0.5em; -} -.cheatsheet-link:hover { - text-decoration: underline; -} -.section-lead { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - max-width: 55ch; - margin-bottom: var(--spacing-lg); -} -.hero-combined { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - min-height: 100vh; - min-height: 100dvh; - padding: var(--spacing-2xl) 0; - background: var(--color-paper); -} -.github-link { - position: absolute; - top: var(--spacing-md); - right: var(--spacing-lg); - z-index: 10; - color: var(--color-ash); - transition: color 0.2s ease; -} -.github-link:hover { - color: var(--color-ink); -} -.github-link svg { - width: 28px; - height: 28px; -} -.hero-combined-container { - max-width: var(--width-max); - margin: 0 auto; - padding: 0 var(--spacing-lg); - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-xl); - align-items: center; - width: 100%; -} -@media (max-width: 1024px) { - .hero-combined-container { - grid-template-columns: 1fr; - gap: var(--spacing-lg); - text-align: center; - } -} -.hero-combined-left { - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} -@media (max-width: 1024px) { - .hero-combined-left { - align-items: center; - } -} -.hero-title-combined { - font-family: var(--font-display); - font-size: clamp(2.5rem, 7vw, 4.5rem); - font-weight: 300; - font-style: italic; - line-height: 1; - letter-spacing: -0.02em; - margin: 0; - color: var(--color-ink); -} -.hero-tagline-combined { - font-family: var(--font-display); - font-size: clamp(1.125rem, 2.5vw, 1.75rem); - font-weight: 400; - font-style: italic; - line-height: 1.3; - margin: 0; - color: var(--color-charcoal); -} -.hero-hook-text { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - max-width: 45ch; - margin: 0; -} -.hero-included-box { - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px 14px; - border: 1px solid var(--color-mist); - background: transparent; - max-width: 45ch; -} -.hero-included-title { - font-family: var(--font-body); - font-size: 0.5625rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.hero-included-items { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - font-size: 0.8125rem; - color: var(--color-charcoal); - line-height: 1.5; -} -.hero-included-items em { - font-style: normal; - font-family: var(--font-mono); - font-size: 0.75rem; -} -.hero-included-sep { - color: var(--color-mist); -} -@media (max-width: 500px) { - .hero-included-items { - flex-direction: column; - align-items: flex-start; - gap: 4px; - } - .hero-included-sep { - display: none; - } -} -.hero-cta-group { - display: flex; - align-items: center; - gap: var(--spacing-lg); - margin-top: var(--spacing-sm); -} -@media (max-width: 600px) { - .hero-cta-group { - flex-direction: column; - gap: var(--spacing-md); - } -} -.hero-cta-combined { - display: inline-block; - padding: var(--spacing-sm) var(--spacing-xl); - font-family: var(--font-body); - font-size: 0.9rem; - font-weight: 500; - letter-spacing: 0.05em; - text-transform: uppercase; - text-decoration: none; - color: var(--color-paper); - background: var(--color-ink); - border: none; - transition: transform 0.2s ease, background 0.2s ease; -} -.hero-cta-combined:hover { - transform: translateY(-2px); - background: var(--color-accent); - color: var(--color-paper); -} -.hero-logos-inline { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.hero-logos-inline .hero-logos-label { - font-size: 0.6875rem; - color: var(--color-ash); - letter-spacing: 0.03em; -} -.hero-logos-inline .hero-logos-row { - display: flex; - align-items: center; - gap: 8px; -} -.hero-logos-inline .hero-logos-row img { - border-radius: 4px; - opacity: 0.7; - transition: opacity 0.2s ease; -} -.hero-logos-inline .hero-logos-row img:hover { - opacity: 1; -} -.hero-combined-right { - display: flex; - justify-content: center; -} -.hero-combined-right .split-comparison { - max-width: 520px; - width: 100%; -} -.hero-combined-right .split-container { - max-width: 100%; -} -.hero-bias-tags { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-xs); - margin-top: var(--spacing-lg); - padding-top: var(--spacing-md); - border-top: 1px solid var(--color-mist); - max-width: var(--width-max); - margin-left: auto; - margin-right: auto; - width: 100%; - padding-bottom: var(--spacing-md); -} -.problem-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.problem-content { - display: grid; - gap: var(--spacing-xl); -} -.split-comparison { - position: relative; - width: 100%; - max-width: 600px; - margin: 0 auto; - padding: 20px; - margin-top: -20px; - margin-bottom: -20px; -} -.split-container { - position: relative; - width: 100%; - max-width: 500px; - height: 380px; - margin: 0 auto; - border-radius: 12px; - overflow: hidden; - background: var(--color-cream); - border: 1px solid var(--color-mist); - cursor: ew-resize; - user-select: none; -} -.split-before, .split-after { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; -} -.split-before { - z-index: 1; -} -.split-content { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} -.split-after { - clip-path: polygon(78% 0%, 100% 0%, 100% 100%, 62% 100%); - z-index: 2; - background: var(--color-paper); -} -.split-divider { - position: absolute; - top: 0; - bottom: 0; - left: 70%; - width: 3px; - background: var(--color-accent); - transform: translateX(-50%) skewX(-10deg); - pointer-events: none; - z-index: 3; - box-shadow: 0 0 20px rgba(0,0,0,0.15); -} -.split-label { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) skewX(10deg); - font-size: 0.6875rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-paper); - background: var(--color-accent); - padding: 6px 14px; - border-radius: 4px; - white-space: nowrap; - box-shadow: 0 2px 8px rgba(0,0,0,0.2); -} -.slop-card { - width: 280px; - height: 280px; - background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #ddd6fe 100%); - border-radius: 16px; - padding: 24px; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - font-family: 'Inter', system-ui, sans-serif; - display: flex; - flex-direction: column; -} -.slop-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 16px; -} -.slop-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: linear-gradient(135deg, #8b5cf6, #7c3aed); - flex-shrink: 0; -} -.slop-text { - flex: 1; -} -.slop-title { - font-size: 14px; - font-weight: 600; - color: #1f2937; - margin-bottom: 2px; -} -.slop-subtitle { - font-size: 12px; - color: #6b7280; -} -.slop-body { - font-size: 13px; - line-height: 1.5; - color: #4b5563; - margin-bottom: auto; - flex: 1; -} -.slop-button { - width: 100%; - padding: 10px 20px; - background: linear-gradient(135deg, #8b5cf6, #7c3aed); - color: white; - border: none; - border-radius: 8px; - font-family: 'Inter', system-ui, sans-serif; - font-size: 13px; - font-weight: 500; - cursor: pointer; - margin-top: auto; -} -.slop-callouts { - position: absolute; - inset: 0; - pointer-events: none; -} -.slop-callout { - position: absolute; - font-size: 0.625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-accent); - background: var(--color-paper); - padding: 4px 8px; - border: 1px solid var(--color-accent); - border-radius: 3px; - white-space: nowrap; - opacity: 0; - animation: calloutFadeIn 0.4s var(--ease-out) forwards; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} -.slop-callout[data-point="font"] { - top: 15%; - right: 5%; - animation-delay: 0.1s; -} -.slop-callout[data-point="gradient"] { - top: 40%; - left: 5%; - animation-delay: 0.25s; -} -.slop-callout[data-point="copy"] { - bottom: 35%; - right: 8%; - animation-delay: 0.4s; -} -.slop-callout[data-point="rounded"] { - bottom: 12%; - left: 10%; - animation-delay: 0.55s; -} -@keyframes calloutFadeIn { - from { - opacity: 0; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - } -} -.impeccable-card { - width: 280px; - height: 300px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - padding: var(--spacing-lg); - text-align: left; - display: flex; - flex-direction: column; -} -.impeccable-eyebrow { - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 500; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--color-accent); - margin-bottom: var(--spacing-xs); -} -.impeccable-title { - font-family: var(--font-display); - font-size: 1.75rem; - font-weight: 300; - font-style: italic; - color: var(--color-ink); - margin-bottom: var(--spacing-sm); - line-height: 1.1; -} -.impeccable-body { - font-size: 0.875rem; - line-height: 1.6; - color: var(--color-ash); - margin-bottom: auto; - flex: 1; -} -.impeccable-button { - display: inline-flex; - margin-top: var(--spacing-sm); - padding: 0.625rem 1.5rem; - background: var(--color-ink); - color: var(--color-paper); - border: none; - font-family: var(--font-body); - font-size: 0.8125rem; - font-weight: 500; - letter-spacing: 0.03em; - cursor: pointer; - transition: all var(--duration-base) var(--ease-out); - align-self: flex-start; -} -.impeccable-button:hover { - background: var(--color-accent); -} -.split-labels { - display: flex; - justify-content: center; - gap: var(--spacing-xl); - margin-top: var(--spacing-md); -} -.split-label-item { - display: flex; - align-items: center; - gap: var(--spacing-xs); - font-size: 0.8125rem; - color: var(--color-ash); -} -.split-label-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--color-mist); -} -.split-label-dot--accent { - background: var(--color-accent); -} -.solution-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.solution-content { - display: grid; - gap: var(--spacing-lg); -} -.solution-content .section-lead { - margin-bottom: 0; -} -.solution-visual { - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: var(--spacing-lg); - align-items: stretch; -} -@media (max-width: 900px) { - .solution-visual { - grid-template-columns: 1fr; - gap: var(--spacing-md); - } -} -.solution-visual-interactive { - width: 100%; - min-height: 380px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 8px; - position: relative; - overflow: hidden; -} -.solution-pillar { - background: var(--color-cream); - border: 1px solid var(--color-mist); - padding: var(--spacing-lg); - transition: all var(--duration-base) var(--ease-out); -} -.solution-pillar:hover { - border-color: var(--color-accent); - transform: translateY(-4px); - box-shadow: 0 20px 60px var(--color-accent-dim); -} -.pillar-header { - text-align: center; - margin-bottom: var(--spacing-lg); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--color-mist); -} -.pillar-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - border-radius: 50%; - background: var(--color-accent-dim); - color: var(--color-accent); - margin-bottom: var(--spacing-sm); -} -.pillar-title { - font-family: var(--font-display); - font-size: 1.75rem; - font-weight: 400; - margin: 0 0 var(--spacing-xs); -} -.pillar-subtitle { - font-size: 0.875rem; - color: var(--color-ash); - margin: 0; -} -.pillar-content { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} -.pillar-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-sm); - background: var(--color-paper); - border-radius: 4px; - transition: all var(--duration-fast) var(--ease-out); -} -.pillar-item:hover { - background: var(--color-accent-dim); -} -.pillar-item-name { - font-weight: 500; - color: var(--color-ink); - font-size: 0.9375rem; -} -.pillar-item-code { - font-family: var(--font-mono); - font-size: 0.875rem; - font-weight: 500; - color: var(--color-accent); - background: transparent; - padding: 0; -} -.pillar-item-desc { - font-size: 0.75rem; - color: var(--color-ash); -} -.pillar-item--more { - justify-content: center; - font-size: 0.8125rem; - font-weight: 500; - color: var(--color-accent); - background: transparent; - border: 1px dashed var(--color-mist); -} -.solution-connector { - display: flex; - align-items: center; - justify-content: center; -} -.connector-plus { - font-family: var(--font-display); - font-size: 3rem; - font-weight: 300; - color: var(--color-accent); - opacity: 0.5; -} -@media (max-width: 900px) { - .solution-connector { - padding: var(--spacing-sm) 0; - } - .connector-plus { - font-size: 2rem; - } -} -.skills-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.skills-gallery { - display: grid; - grid-template-columns: 200px 1fr; - gap: var(--spacing-xl); - align-items: start; -} -@media (max-width: 968px) { - .skills-gallery { - grid-template-columns: 1fr; - gap: var(--spacing-lg); - } -} -.skills-nav { - display: flex; - flex-direction: column; - gap: 2px; - position: sticky; - top: var(--spacing-lg); -} -@media (max-width: 968px) { - .skills-nav { - flex-direction: row; - flex-wrap: wrap; - gap: var(--spacing-xs); - position: static; - } -} -.skill-nav-item { - padding: var(--spacing-sm) var(--spacing-md); - background: transparent; - border: none; - border-left: 2px solid transparent; - color: var(--color-ash); - font-family: var(--font-body); - font-size: 0.9375rem; - font-weight: 400; - cursor: pointer; - transition: all 0.2s ease; - text-align: left; - text-decoration: none; - display: block; -} -.skill-nav-item:hover { - color: var(--color-text); - background: var(--color-cream); -} -.skill-nav-item.active { - color: var(--color-accent); - border-left-color: var(--color-accent); - background: var(--color-accent-dim); - font-weight: 500; -} -@media (max-width: 968px) { - .skill-nav-item { - border-left: none; - border-bottom: 2px solid transparent; - padding: var(--spacing-xs) var(--spacing-md); - } - .skill-nav-item.active { - border-bottom-color: var(--color-accent); - } -} -.skills-showcase { - display: grid; - grid-template-columns: 1.2fr 1fr; - gap: var(--spacing-lg); - align-items: start; -} -@media (max-width: 1100px) { - .skills-showcase { - grid-template-columns: 1fr; - } -} -.loading-state { - padding: var(--spacing-xl); - text-align: center; - color: var(--color-ash); - font-style: italic; -} -.mobile-commands-layout { - display: none; -} -@media (max-width: 900px) { - .mobile-commands-layout { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - } - .commands-container { - display: none; - } -} -.mobile-carousel-wrapper { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - padding: var(--spacing-xs) 0; -} -.mobile-carousel-wrapper::-webkit-scrollbar { - display: none; -} -.mobile-carousel { - display: flex; - gap: var(--spacing-xs); - padding-right: var(--spacing-md); -} -.mobile-cmd-pill { - flex-shrink: 0; - padding: var(--spacing-sm) var(--spacing-md); - min-height: 44px; - font-family: var(--font-mono); - font-size: 0.8125rem; - font-weight: 500; - color: var(--color-ash); - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 100px; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} -.mobile-cmd-pill:hover { - color: var(--color-charcoal); - border-color: var(--color-charcoal); -} -.mobile-cmd-pill.active { - color: var(--color-paper); - background: var(--color-ink); - border-color: var(--color-ink); -} -.mobile-demo-area { - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 8px; - padding: var(--spacing-sm); -} -.mobile-demo-area .demo-split-comparison { - width: 100%; -} -.mobile-demo-area .split-container { - width: 100%; - max-width: 100%; - height: 320px; -} -.mobile-demo-area .demo-caption { - font-size: 0.75rem; - margin-top: var(--spacing-sm); -} -.mobile-info-area { - padding-top: var(--spacing-sm); -} -.mobile-cmd-info { - display: none; - padding: var(--spacing-sm) 0; -} -.mobile-cmd-info.active { - display: block; -} -.mobile-cmd-name { - font-family: var(--font-mono); - font-size: 1.125rem; - font-weight: 600; - color: var(--color-ink); - margin: 0 0 var(--spacing-xs) 0; -} -.mobile-cmd-desc { - font-size: 0.875rem; - color: var(--color-charcoal); - line-height: 1.5; - margin: 0; -} -.mobile-cmd-rel { - margin-top: var(--spacing-xs); - font-size: 0.75rem; - color: var(--color-ash); -} -.mobile-cmd-rel code { - font-family: var(--font-mono); - color: var(--color-ink); -} -.downloads-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.downloads-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-lg); -} -.download-card { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding: var(--spacing-lg); - background: var(--color-cream); - border: 1px solid var(--color-mist); - transition: all var(--duration-base) var(--ease-out); -} -.download-card:hover { - border-color: var(--color-accent); - transform: translateY(-4px); - box-shadow: 0 20px 60px var(--color-accent-dim); -} -.download-card-icon { - margin-bottom: var(--spacing-sm); -} -.download-card-icon img { - width: 40px; - height: 40px; - object-fit: contain; - border-radius: 8px; -} -.download-card-title { - font-family: var(--font-display); - font-size: 1.25rem; - font-weight: 400; - margin: 0 0 var(--spacing-sm) 0; -} -.download-card-note { - font-size: 0.75rem; - color: var(--color-ash); - margin-bottom: var(--spacing-xs); -} -.download-card .btn { - margin-top: var(--spacing-xs); -} -.install-command { - display: flex; - align-items: center; - gap: var(--spacing-xs); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; - padding: var(--spacing-sm); - margin-top: var(--spacing-sm); - width: 100%; -} -.install-command code { - flex: 1; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--color-ink); - background: transparent; - padding: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.copy-btn { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: transparent; - border: 1px solid var(--color-mist); - border-radius: 4px; - color: var(--color-ash); - cursor: pointer; - flex-shrink: 0; - transition: all var(--duration-fast) var(--ease-out); -} -.copy-btn:hover { - background: var(--color-accent-dim); - border-color: var(--color-accent); - color: var(--color-accent); -} -.copy-btn.copied { - background: var(--color-accent); - border-color: var(--color-accent); - color: var(--color-paper); -} -.install-hint { - font-size: 0.75rem; - color: var(--color-ash); - margin: var(--spacing-xs) 0 0 0; -} -.install-hint code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; -} -.download-card-details { - width: 100%; - margin-top: var(--spacing-sm); - font-size: 0.8125rem; - text-align: left; -} -.download-card-details summary { - cursor: pointer; - color: var(--color-ash); - font-size: 0.75rem; - padding: var(--spacing-xs) 0; - list-style: none; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; -} -.download-card-details summary::before { - content: '▶'; - font-size: 0.5rem; - transition: transform var(--duration-fast) var(--ease-out); -} -.download-card-details[open] summary::before { - transform: rotate(90deg); -} -.download-card-details summary::-webkit-details-marker { - display: none; -} -.download-card-details ol { - margin: var(--spacing-sm) 0; - padding-left: var(--spacing-md); - color: var(--color-charcoal); - line-height: 1.6; -} -.download-card-details li { - margin-bottom: 4px; -} -.download-card-details code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; -} -.download-card-details a { - color: var(--color-accent); - text-decoration: none; - font-size: 0.75rem; -} -.download-card-details a:hover { - text-decoration: underline; -} -.opensource-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); - text-align: center; -} -.opensource-content { - max-width: 500px; - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-md); -} -.opensource-title { - font-size: clamp(1.5rem, 4vw, 2rem); - font-weight: 300; -} -.opensource-desc { - font-size: 1.125rem; - color: var(--color-ash); - line-height: 1.6; -} -.site-footer { - border-top: 1px solid var(--color-mist); - padding: var(--spacing-xl) var(--spacing-lg); - background: var(--color-cream); -} -.footer-content { - max-width: var(--width-max); - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: var(--spacing-lg); -} -@media (max-width: 768px) { - .footer-content { - flex-direction: column; - text-align: center; - } -} -.footer-brand { - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} -.footer-logo { - font-family: var(--font-display); - font-size: 1.25rem; - font-weight: 400; - color: var(--color-ink); -} -.footer-tagline { - font-size: 0.875rem; - color: var(--color-ash); -} -.footer-links { - display: flex; - gap: var(--spacing-lg); - flex-wrap: wrap; -} -.footer-links a { - font-size: 0.875rem; - color: var(--color-ash); - transition: color var(--duration-fast) var(--ease-out); -} -.footer-links a:hover { - color: var(--color-accent); -} -.footer-author { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-md); - flex-wrap: wrap; - width: 100%; - padding-top: var(--spacing-lg); - margin-top: var(--spacing-md); - border-top: 1px solid var(--color-mist); -} -.footer-author-label { - font-size: 0.875rem; - color: var(--color-ash); -} -.footer-author-label a { - color: var(--color-text); - transition: color var(--duration-fast) var(--ease-out); -} -.footer-author-label a:hover { - color: var(--color-accent); -} -.footer-author-links { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.footer-social-link { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - color: var(--color-ash); - background: transparent; - border-radius: 50%; - transition: all var(--duration-fast) var(--ease-out); - text-decoration: none; -} -.footer-social-link:hover { - color: var(--color-accent); - background: var(--color-accent-dim); -} -.footer-author-divider { - width: 1px; - height: 20px; - background: var(--color-mist); - margin: 0 var(--spacing-xs); -} -.footer-newsletter { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 0.875rem; - font-weight: 500; - color: var(--color-text); - text-decoration: none; - padding: 8px 14px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 100px; - transition: all var(--duration-fast) var(--ease-out); -} -.footer-newsletter:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} -.footer-newsletter svg { - transition: transform var(--duration-fast) var(--ease-out); -} -.footer-newsletter:hover svg { - transform: translateX(3px); -} -@media (max-width: 768px) { - .footer-author { - flex-direction: column; - gap: var(--spacing-sm); - } - .footer-author-divider { - display: none; - } -} -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-xs); - padding: 1rem 2rem; - font-family: var(--font-body); - font-size: 0.9375rem; - font-weight: 600; - letter-spacing: 0.03em; - border: none; - cursor: pointer; - transition: all var(--duration-base) var(--ease-out); - position: relative; - overflow: hidden; - text-decoration: none; -} -.btn-primary { - background: var(--color-ink); - color: var(--color-paper); -} -.btn-primary::before { - content: ''; - position: absolute; - inset: 0; - background: var(--color-accent); - transform: translateY(100%); - transition: transform var(--duration-base) var(--ease-out); - z-index: 0; -} -.btn-primary:hover::before { - transform: translateY(0); -} -.btn-primary:hover { - color: var(--color-paper); -} -.btn-primary span, .btn-primary svg { - position: relative; - z-index: 1; -} -.btn-primary:not(:has(span)) { - position: relative; - z-index: 1; -} -.btn-secondary { - background: transparent; - color: var(--color-ink); - border: 1px solid var(--color-ink); -} -.btn-secondary:hover { - background: var(--color-ink); - color: var(--color-paper); -} -.btn:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.btn-primary:focus-visible { - outline-color: var(--color-paper); - box-shadow: 0 0 0 4px var(--color-accent); -} -.btn-secondary:focus-visible { - outline-color: var(--color-accent); -} -@keyframes revealUp { - to { - opacity: 1; - transform: translateY(0); - } -} -@keyframes fadeIn { - to { - opacity: 1; - } -} -@keyframes float { - 0%, 100% { - transform: translateX(-50%) translateY(0); - } - 50% { - transform: translateX(-50%) translateY(-8px); - } -} -@keyframes bounce { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(4px); - } -} -[data-reveal] { - opacity: 0; - transform: translateY(30px); - transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out); -} -[data-reveal].revealed { - opacity: 1; - transform: translateY(0); -} -[data-reveal]:nth-child(1) { - transition-delay: 0s; -} -[data-reveal]:nth-child(2) { - transition-delay: 0.1s; -} -[data-reveal]:nth-child(3) { - transition-delay: 0.2s; -} -[data-reveal]:nth-child(4) { - transition-delay: 0.3s; -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - html { - scroll-behavior: auto; - } - .hero-canvas { - display: none; - } - .hero-scroll-indicator { - animation: none; - opacity: 1; - } - [data-reveal] { - opacity: 1; - transform: none; - } - .gallery-frame { - opacity: 1; - transform: none; - } -} -.load-error { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: var(--spacing-2xl) var(--spacing-lg); - gap: var(--spacing-md); - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 8px; -} -.load-error-icon { - font-size: 2.5rem; - color: var(--color-accent); -} -.load-error-title { - font-family: var(--font-display); - font-size: 1.5rem; - font-weight: 400; - color: var(--color-ink); - margin: 0; -} -.load-error-text { - font-size: 1rem; - color: var(--color-ash); - max-width: 40ch; - line-height: 1.5; -} -.load-error-retry { - margin-top: var(--spacing-sm); -} -.bias-tags { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-sm); - margin-top: var(--spacing-lg); -} -.bias-tags-label { - font-family: var(--font-mono); - font-size: 0.6875rem; - font-weight: 500; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-ash); -} -.bias-tags-list { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: var(--spacing-xs); -} -.bias-tag { - font-size: 0.75rem; - font-weight: 500; - padding: 6px 12px; - background: var(--color-cream); - border: 1px solid var(--color-mist); - color: var(--color-charcoal); - transition: all var(--duration-fast) var(--ease-out); -} -.bias-tag:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} -.antidote-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.patterns-categories { - margin-bottom: var(--spacing-xl); -} -.pattern-tabs { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-lg); -} -.pattern-tab { - background: none; - border: none; - border-bottom: 2px solid transparent; - font-family: var(--font-body); - font-size: 0.875rem; - color: var(--color-ash); - padding: var(--spacing-xs) var(--spacing-sm); - cursor: pointer; - transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out); -} -.pattern-tab:hover { - color: var(--color-charcoal); -} -.pattern-tab.active { - color: var(--color-ink); - font-weight: 500; - border-bottom-color: var(--color-accent); -} -.pattern-tab:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; - border-radius: 4px; -} -.pattern-panel { - display: none; -} -.pattern-panel.active { - display: block; -} -.pattern-columns { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-xl); -} -@media (max-width: 768px) { - .pattern-columns { - grid-template-columns: 1fr; - gap: var(--spacing-md); - } -} -.pattern-column-label { - display: block; - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: var(--spacing-sm); - color: var(--color-ash); -} -.pattern-column--anti .pattern-column-label { - color: var(--color-accent); -} -.pattern-column--do .pattern-column-label { - color: var(--color-success, #22c55e); -} -.pattern-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} -.pattern-item { - font-size: 0.8125rem; - padding-left: var(--spacing-md); - position: relative; - line-height: 1.5; -} -.pattern-item--anti { - color: var(--color-ash); -} -.pattern-item--anti::before { - content: '×'; - position: absolute; - left: 0; - color: var(--color-accent); - font-weight: 600; -} -.pattern-item--do { - color: var(--color-charcoal); -} -.pattern-item--do::before { - content: '✓'; - position: absolute; - left: 0; - color: var(--color-success, #22c55e); - font-weight: 600; -} -.contribute-inline { - font-size: 0.875rem; - color: var(--color-ash); - margin-top: var(--spacing-md); -} -.contribute-inline a { - color: var(--color-accent); - text-decoration: none; -} -.contribute-inline a:hover { - text-decoration: underline; -} -.pillar-item--main { - background: var(--color-accent-dim); - border: 1px solid var(--color-accent); -} -.pillar-item--main .pillar-item-name { - font-size: 1.125rem; - font-weight: 600; - color: var(--color-accent); -} -.pillar-item--ref { - background: transparent; - padding: var(--spacing-xs) var(--spacing-sm); -} -.pillar-item-label { - font-size: 0.75rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ash); -} -.pillar-refs { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - padding: 0 var(--spacing-sm); -} -.pillar-ref { - font-size: 0.6875rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.03em; - padding: 4px 10px; - background: var(--color-paper); - color: var(--color-ash); - border: 1px solid var(--color-mist); - border-radius: 3px; - transition: all var(--duration-fast) var(--ease-out); -} -.pillar-ref:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} -.pillar-command-group { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-sm); - background: var(--color-paper); - border-radius: 4px; -} -.pillar-group-label { - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ash); - width: 100%; - margin-bottom: 4px; -} -.pillar-command-group .pillar-item-code { - font-size: 0.8125rem; - padding: 4px 8px; - background: var(--color-accent-dim); - border-radius: 3px; -} -.platforms-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.platforms-section .section-subtitle { - max-width: 60ch; -} -.install-terminal { - max-width: 640px; - margin: 0 auto var(--spacing-xl); -} -.install-terminal .glass-terminal { - height: auto; -} -.install-terminal .terminal-body { - padding: 0; - display: flex; - flex-direction: column; -} -.install-terminal-row { - display: flex; - flex-direction: column; - gap: 6px; - padding: var(--spacing-md) var(--spacing-lg); -} -.install-terminal-label { - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.install-terminal-cmd { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.install-terminal-cmd .terminal-prompt { - flex-shrink: 0; -} -.install-terminal-cmd code { - flex: 1; - font-family: var(--font-mono); - font-size: 0.9375rem; - color: var(--color-ink); - background: transparent; - padding: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.install-terminal-cmd .copy-btn { - flex-shrink: 0; -} -.install-terminal-cmd .btn { - padding: 0.5rem 1rem; - font-size: 0.8125rem; -} -.install-terminal-note { - font-size: 0.75rem; - color: var(--color-ash); - padding-left: calc(0.75rem + var(--spacing-sm)); -} -.install-terminal-note code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; - color: var(--color-ink); -} -.install-terminal-divider { - height: 1px; - background: var(--color-mist); - margin: 0; -} -@media (max-width: 600px) { - .install-terminal-row { - padding: var(--spacing-sm) var(--spacing-md); - } - .install-terminal-cmd code { - font-size: 0.75rem; - } -} -.install-providers { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-sm); - margin-top: var(--spacing-md); -} -.install-providers-label { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.install-providers-row { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: var(--spacing-sm); -} -.install-provider-badge { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.8125rem; - color: var(--color-charcoal); -} -.install-provider-badge img { - border-radius: 4px; -} -.install-terminal-cmd--download { - display: flex; - align-items: center; - gap: var(--spacing-sm); - flex-wrap: wrap; -} -.prefix-toggle { - display: inline-flex; - align-items: center; - gap: 8px; - cursor: pointer; - padding: 6px 12px; - background: rgba(0, 0, 0, 0.06); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 6px; - transition: border-color var(--duration-fast) var(--ease-out); -} -.prefix-toggle:hover { - border-color: rgba(0, 0, 0, 0.2); -} -.prefix-toggle input { - position: absolute; - opacity: 0; - width: 0; - height: 0; -} -.prefix-toggle-slider { - position: relative; - width: 32px; - height: 18px; - background: var(--color-mist); - border-radius: 18px; - transition: background var(--duration-fast) var(--ease-out); - flex-shrink: 0; -} -.prefix-toggle-slider::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - width: 14px; - height: 14px; - background: var(--color-paper); - border-radius: 50%; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); - transition: transform var(--duration-fast) var(--ease-out); -} -.prefix-toggle input:checked + .prefix-toggle-slider { - background: var(--color-accent); -} -.prefix-toggle input:checked + .prefix-toggle-slider::after { - transform: translateX(14px); -} -.prefix-toggle input:focus-visible + .prefix-toggle-slider { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.prefix-toggle-label { - font-size: 0.75rem; - color: var(--color-charcoal); - white-space: nowrap; -} -.prefix-toggle-label code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-accent-dim); - color: var(--color-accent); - padding: 1px 4px; - border-radius: 3px; -} -.has-tooltip { - position: relative; - cursor: default; -} -.has-tooltip::after { - content: attr(data-tooltip); - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - padding: 6px 10px; - background: var(--color-ink); - color: var(--color-paper); - font-size: 0.6875rem; - line-height: 1.4; - border-radius: 6px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s ease; - z-index: 100; -} -.has-tooltip:hover::after { - opacity: 1; -} -.hero-logo-icon { - display: inline-flex; - align-items: center; -} -.download-tip { - font-size: 0.8125rem; - color: var(--color-ash); - margin-top: var(--spacing-sm); - text-align: center; -} -.download-tip a { - color: var(--color-accent); - text-decoration: none; -} -.download-tip a:hover { - text-decoration: underline; -} -.consulting-section { - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-mist); -} -.consulting-content { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-lg); - flex-wrap: wrap; -} -.consulting-actions { - display: flex; - gap: var(--spacing-sm); - flex-shrink: 0; -} -.consulting-text { - flex: 1; - min-width: 280px; -} -.consulting-title { - font-size: clamp(1.5rem, 4vw, 2rem); - font-weight: 300; - font-style: italic; - margin: 0 0 var(--spacing-sm) 0; -} -.consulting-desc { - font-size: 1rem; - color: var(--color-charcoal); - line-height: 1.6; - margin: 0; - max-width: 45ch; -} -@media (max-width: 600px) { - .consulting-content { - flex-direction: column; - align-items: flex-start; - } - .consulting-actions { - flex-direction: column; - width: 100%; - } - .consulting-actions .btn { - width: 100%; - justify-content: center; - } -} +.split-container:before{content:"";background-image:linear-gradient(var(--color-mist) 1px, transparent 1px), linear-gradient(90deg, var(--color-mist) 1px, transparent 1px);opacity:.3;pointer-events:none;background-size:20px 20px;position:absolute;inset:0}.split-container:after{content:"← Drag →";letter-spacing:.1em;text-transform:uppercase;color:var(--color-ash);background:var(--color-paper);opacity:.8;z-index:10;border-radius:4px;padding:4px 12px;font-size:.625rem;font-weight:600;transition:opacity .3s;position:absolute;bottom:12px;left:50%;transform:translate(-50%)}.split-container:hover:after{opacity:0}.split-after .impeccable-card{box-shadow:0 10px 40px #00000014}@keyframes splitEntry{0%{opacity:0;transform:translate(-50%)skew(-10deg)scaleY(.8)}to{opacity:1;transform:translate(-50%)skew(-10deg)scaleY(1)}}.split-divider{animation:splitEntry .6s var(--ease-out) .3s backwards}.split-label-item{transition:color var(--duration-fast) var(--ease-out);cursor:default}.split-label-item:hover{color:var(--color-text)}.split-label-item[data-point=after]:hover .split-label-dot--accent{transform:scale(1.3)}.split-label-dot{transition:transform var(--duration-fast) var(--ease-spring)}.split-badge{letter-spacing:.08em;text-transform:uppercase;z-index:5;pointer-events:none;border-radius:3px;padding:3px 8px;font-size:.625rem;font-weight:600;position:absolute;top:10px}.split-badge--before{color:var(--color-ash);background:var(--color-paper);border:1px solid var(--color-mist);left:10px}.split-badge--after{color:var(--color-paper);background:var(--color-accent);right:10px}@media (hover:none){.split-container:after{content:"← Swipe →"}}@media (max-width:600px){.split-label{padding:4px 10px;font-size:.5625rem}}.commands-section{padding:var(--spacing-xl) 0;background:var(--color-paper);position:relative}.commands-gallery{display:block}.commands-container{gap:var(--spacing-2xl);grid-template-columns:1fr 1.2fr;align-items:start;display:grid}@media (max-width:900px){.commands-container{grid-template-columns:1fr}}.command-manual{gap:var(--spacing-sm);flex-direction:column;padding-bottom:20vh;display:flex}.command-category-header{font-family:var(--font-display);color:var(--color-accent);text-transform:uppercase;letter-spacing:.1em;padding:var(--spacing-lg) var(--spacing-lg) var(--spacing-sm);margin-top:var(--spacing-md);border-bottom:1px solid var(--color-mist);font-size:.875rem;font-weight:600}.command-category-header:first-child{margin-top:0}.manual-entry{padding:var(--spacing-lg);padding-left:calc(var(--spacing-lg) + 16px);border-left:2px solid var(--color-mist);transition:border-color .4s var(--ease-out), opacity .4s var(--ease-out), background .4s var(--ease-out), transform .4s var(--ease-out);opacity:.4;cursor:pointer;position:relative;transform:translate(-16px)}.manual-entry:hover{opacity:.7}.manual-entry.active{border-left-color:var(--color-accent);opacity:1;background:linear-gradient(to right, var(--color-bg), transparent);transform:translate(0)}.manual-cmd-name{font-family:var(--font-mono);margin:0 0 var(--spacing-sm);color:var(--color-ink);font-size:1.5rem;font-weight:500}.beta-badge{font-family:var(--font-body);letter-spacing:.08em;text-transform:uppercase;color:var(--color-accent);border:1px solid var(--color-accent);vertical-align:middle;border-radius:3px;margin-left:6px;padding:1px 5px;font-size:.55rem;font-weight:600}.manual-cmd-desc{color:var(--color-charcoal);margin:0;font-size:1rem;line-height:1.6}.manual-cmd-rel{color:var(--color-ash);margin-top:var(--spacing-sm);flex-wrap:wrap;align-items:center;gap:.5ch;font-size:.8125rem;display:flex}.manual-cmd-rel .rel-icon{color:var(--color-accent);font-weight:600}.manual-cmd-rel code{font-family:var(--font-mono);background:var(--color-mist);color:var(--color-ink);border-radius:3px;padding:2px 6px;font-size:.75rem}.glass-terminal-wrapper{top:var(--spacing-xl);height:calc(100vh - var(--spacing-xl) * 2);min-height:500px;max-height:800px;position:sticky}.terminal-stack{perspective:1200px;height:100%;position:relative}.terminal-stack-tabs{z-index:10;gap:4px;display:flex;position:absolute;top:-31px;right:8px}.terminal-stack-tab{font-family:var(--font-mono);background:var(--color-cream);border:1px solid var(--color-mist);color:var(--color-ash);cursor:pointer;border-bottom:none;border-radius:6px 6px 0 0;padding:5px 12px;font-size:.75rem;transition:all .2s}.terminal-stack-tab:hover{background:var(--color-paper);color:var(--color-charcoal)}.terminal-stack-tab.active{background:var(--color-paper);color:var(--color-ink);border-color:var(--color-mist)}.terminal-window{transform-origin:bottom;transition:transform .4s cubic-bezier(.4,0,.2,1),opacity .3s,filter .3s;position:absolute;inset:0}.terminal-window--demo{z-index:2}.terminal-window--demo.is-back{opacity:.6;filter:brightness(.92);pointer-events:none;z-index:1;transform:translateY(16px)translate(12px)scale(.96)}.terminal-window--source{z-index:1;opacity:.6;filter:brightness(.92);pointer-events:none;transform:translateY(16px)translate(12px)scale(.96)}.terminal-window--source.is-front{opacity:1;filter:brightness();pointer-events:auto;z-index:2;transform:translateY(0)translate(0)scale(1)}.source-window{background:var(--color-paper);-webkit-backdrop-filter:blur(12px);border:1px solid var(--color-mist);border-radius:8px;flex-direction:column;height:100%;display:flex;overflow:hidden;box-shadow:0 20px 60px -10px #00000026}.source-header{background:var(--color-cream);border-bottom:1px solid var(--color-mist);flex-shrink:0;align-items:center;gap:8px;padding:12px 16px;display:flex}.source-title{font-family:var(--font-mono);color:var(--color-ink);font-size:.875rem;font-weight:500}.source-body{padding:var(--spacing-md);font-family:var(--font-mono);color:var(--color-charcoal);overscroll-behavior:contain;white-space:pre-wrap;word-break:break-word;background:var(--color-cream);flex:1;font-size:.75rem;line-height:1.5;overflow-y:auto}.source-loading{color:var(--color-ash);font-style:italic}@media (max-width:900px){.glass-terminal-wrapper{display:none}}.glass-terminal{background:var(--color-paper);-webkit-backdrop-filter:blur(12px);border:1px solid var(--color-mist);border-radius:8px;flex-direction:column;height:100%;display:flex;overflow:hidden;box-shadow:0 20px 60px -10px #00000026}.terminal-header{background:var(--color-cream);border-bottom:1px solid var(--color-mist);align-items:center;gap:8px;padding:12px 16px;display:flex}.terminal-dot{border-radius:50%;width:10px;height:10px}.terminal-dot.red{background:#ff5f56}.terminal-dot.yellow{background:#ffbd2e}.terminal-dot.green{background:#27c93f}.terminal-title{font-family:var(--font-mono);color:var(--color-ash);margin-left:auto;font-size:.75rem}.terminal-body{padding:var(--spacing-md);font-family:var(--font-mono);color:var(--color-ink);flex-direction:column;flex:1;min-height:0;font-size:.9375rem;display:flex;overflow-y:auto}.terminal-line{margin-bottom:var(--spacing-sm);gap:var(--spacing-sm);line-height:1.5;display:flex}.terminal-prompt{color:var(--color-accent);-webkit-user-select:none;user-select:none;font-weight:700}.terminal-cursor{background:var(--color-accent);vertical-align:middle;width:8px;height:1.2em;animation:1s step-end infinite blink;display:inline-block}.terminal-output{color:var(--color-ash);margin-bottom:var(--spacing-md);white-space:pre-wrap}@media (max-height:800px){.terminal-output{display:none}}.terminal-cmd{color:var(--color-accent);font-weight:600}.terminal-step{color:var(--color-charcoal)}.terminal-done{color:var(--color-success,#22c55e);font-weight:500}.terminal-preview{background:var(--color-paper);margin:var(--spacing-sm) 0;border-radius:12px;flex:1;min-height:0;overflow:hidden}.terminal-cursor-line{flex-shrink:0;margin-top:var(--spacing-sm)!important}.terminal-preview .demo-split-comparison{flex-direction:column;height:100%;display:flex}.terminal-preview .demo-split-comparison .split-container{cursor:ew-resize;-webkit-user-select:none;user-select:none;background:var(--color-cream);flex:1;min-height:0;position:relative;overflow:hidden}.terminal-preview .demo-split-comparison .split-before,.terminal-preview .demo-split-comparison .split-after{padding:var(--spacing-md);justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.terminal-preview .demo-split-comparison .split-before{z-index:1;background:var(--color-cream)}.terminal-preview .demo-split-comparison .split-after{z-index:2;background:var(--color-paper);clip-path:polygon(58% 0%,100% 0%,100% 100%,42% 100%)}.terminal-preview .demo-split-comparison .split-content{flex-direction:column;justify-content:center;align-items:center;width:100%;max-width:280px;display:flex}.terminal-preview .demo-split-comparison .split-divider{background:var(--color-accent);pointer-events:none;z-index:3;width:2px;position:absolute;top:0;bottom:0;left:50%;transform:translate(-50%)skew(-10deg);box-shadow:0 0 12px #0000001a}.terminal-preview .demo-split-comparison .split-label{letter-spacing:.08em;text-transform:uppercase;color:var(--color-paper);background:var(--color-accent);white-space:nowrap;border-radius:3px;padding:4px 10px;font-size:.5625rem;font-weight:600;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)skew(10deg)}.terminal-preview .demo-split-comparison .demo-caption{color:var(--color-ash);text-align:center;padding:var(--spacing-sm) var(--spacing-md);flex-shrink:0;font-size:.75rem}@keyframes blink{50%{opacity:0}}.casestudies-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist);position:relative}.transformations-tabbed{margin-top:var(--spacing-xl)}.transformation-tabs{gap:var(--spacing-xs);border-bottom:1px solid var(--color-mist);margin-bottom:var(--spacing-lg);display:flex}.transformation-tab{font-family:var(--font-display);color:var(--color-ash);padding:var(--spacing-sm) var(--spacing-md);cursor:pointer;background:0 0;border:none;font-size:.9375rem;font-weight:500;transition:color .2s;position:relative}.transformation-tab:hover{color:var(--color-charcoal)}.transformation-tab.active{color:var(--color-ink)}.transformation-tab.active:after{content:"";background:var(--color-accent);height:2px;position:absolute;bottom:-1px;left:0;right:0}.transformation-panels{position:relative}.transformation-panel{gap:var(--spacing-lg);flex-direction:column;animation:.3s fadeInPanel;display:none}.transformation-panel.active{display:flex}@keyframes fadeInPanel{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.transformation-images{align-items:center;gap:var(--spacing-md);display:flex}.transformation-before,.transformation-after{flex:1;margin:0}.transformation-before img,.transformation-after img,.transformation-placeholder{aspect-ratio:16/10;object-fit:cover;border:1px solid var(--color-mist);cursor:pointer;border-radius:8px;width:100%;transition:transform .2s,box-shadow .2s}.transformation-before img:hover,.transformation-after img:hover,.transformation-placeholder:hover{transform:scale(1.02);box-shadow:0 8px 24px -4px #00000026}.transformation-placeholder{background:linear-gradient(135deg, var(--color-mist) 0%, var(--color-cream) 100%);color:var(--color-ash);justify-content:center;align-items:center;font-size:.8125rem;font-style:italic;display:flex}.transformation-before figcaption,.transformation-after figcaption{text-transform:uppercase;letter-spacing:.05em;color:var(--color-ash);margin-top:var(--spacing-xs);text-align:center;font-size:.75rem;font-weight:600}.transformation-arrow{color:var(--color-accent);flex-shrink:0;font-size:1.5rem;font-weight:300}.transformation-info{max-width:600px}.transformation-title{font-family:var(--font-display);color:var(--color-ink);margin:0 0 var(--spacing-xs);font-size:1.25rem;font-weight:600}.transformation-desc{color:var(--color-charcoal);margin:0 0 var(--spacing-sm);font-size:.9375rem;line-height:1.6}.transformation-commands{flex-wrap:wrap;gap:6px;display:flex}.transformation-command{font-family:var(--font-mono);background:var(--color-mist);color:var(--color-charcoal);border-radius:4px;padding:4px 10px;font-size:.75rem}.lightbox{z-index:1000;opacity:0;visibility:hidden;background:#000000e6;justify-content:center;align-items:center;transition:opacity .3s,visibility .3s;display:flex;position:fixed;inset:0}.lightbox.active{opacity:1;visibility:visible}.lightbox-close{color:#fff;cursor:pointer;opacity:.7;background:0 0;border:none;font-size:2.5rem;line-height:1;transition:opacity .2s;position:absolute;top:20px;right:24px}.lightbox-close:hover{opacity:1}.lightbox-image{object-fit:contain;border-radius:8px;max-width:90vw;max-height:85vh;box-shadow:0 20px 60px #00000080}@media (max-width:768px){.transformation-images{flex-direction:column}.transformation-arrow{transform:rotate(90deg)}.transformation-before,.transformation-after{width:100%}}.hero-version-link{color:var(--color-ash);margin-top:var(--spacing-sm);font-size:.8125rem}.hero-version-link a{color:var(--color-ash);border-bottom:1px solid #0000;text-decoration:none;transition:color .2s,border-color .2s}.hero-version-link a:hover{color:var(--color-accent);border-bottom-color:var(--color-accent)}.changelog-section{padding:var(--spacing-xl) 0;border-top:1px solid var(--color-mist);position:relative}.changelog-list{flex-direction:column;gap:0;display:flex}.changelog-entry{padding:var(--spacing-md) 0;border-bottom:1px solid var(--color-mist)}.changelog-entry:first-child{border-top:1px solid var(--color-mist)}.changelog-version-header{align-items:baseline;gap:var(--spacing-sm);margin-bottom:var(--spacing-sm);display:flex}.changelog-version{font-family:var(--font-mono);color:var(--color-ink);font-size:1.125rem;font-weight:600}.changelog-date{color:var(--color-ash);font-size:.8125rem}.changelog-items{padding-left:var(--spacing-md);color:var(--color-charcoal);margin:0;line-height:1.7}.changelog-items li{margin-bottom:var(--spacing-xs)}.changelog-items code{font-family:var(--font-mono);background:var(--color-mist);color:var(--color-ink);border-radius:3px;padding:2px 6px;font-size:.875em}.faq-section{padding:var(--spacing-xl) 0;border-top:1px solid var(--color-mist);position:relative}.faq-list{flex-direction:column;gap:0;display:flex}.faq-item{border-bottom:1px solid var(--color-mist)}.faq-item:first-child{border-top:1px solid var(--color-mist)}.faq-question{font-family:var(--font-display);color:var(--color-ink);padding:var(--spacing-md) 0;cursor:pointer;justify-content:space-between;align-items:center;font-size:1.125rem;font-weight:500;list-style:none;transition:color .2s;display:flex}.faq-question::-webkit-details-marker{display:none}.faq-question:after{content:"+";font-family:var(--font-body);color:var(--color-accent);transition:transform .3s var(--ease-out);font-size:1.5rem;font-weight:300}.faq-item[open] .faq-question:after{transform:rotate(45deg)}.faq-question:hover{color:var(--color-accent)}.faq-answer{padding:0 0 var(--spacing-md);color:var(--color-charcoal);animation:faqFadeIn .3s var(--ease-out);line-height:1.7}.faq-answer p{margin:0 0 var(--spacing-sm)}.faq-answer p:last-child{margin-bottom:0}.faq-answer ul{margin:var(--spacing-sm) 0;padding-left:var(--spacing-md)}.faq-answer li{margin-bottom:var(--spacing-xs)}.faq-answer code{font-family:var(--font-mono);background:var(--color-mist);color:var(--color-ink);border-radius:3px;padding:2px 6px;font-size:.875em}.faq-answer a{color:var(--color-accent);border-bottom:1px solid #0000;text-decoration:none;transition:border-color .2s}.faq-answer a:hover{border-bottom-color:var(--color-accent)}@keyframes faqFadeIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.skills-section{padding:var(--spacing-xl) 0;background:var(--color-bg);position:relative;overflow:hidden}.skills-gallery{display:block;position:relative}.gallery-track{gap:var(--spacing-lg);scroll-snap-type:x mandatory;padding:var(--spacing-md) var(--spacing-lg) var(--spacing-xl);-webkit-overflow-scrolling:touch;scrollbar-width:none;cursor:grab;display:flex;overflow-x:auto}.gallery-track:active{cursor:grabbing}.gallery-track::-webkit-scrollbar{display:none}.gallery-frame{scroll-snap-align:center;background:var(--color-paper);border:1px solid var(--color-mist);opacity:.4;max-width:1100px;transition:opacity .6s var(--ease-out), transform .6s var(--ease-out), box-shadow .6s var(--ease-out);border-radius:2px;flex:0 0 80vw;position:relative;overflow:hidden;transform:scale(.95);box-shadow:0 4px 6px -1px #0000000d,0 20px 50px -10px #0000001a}.gallery-frame.active{opacity:1;border-color:var(--color-charcoal);border-width:1px;transform:scale(1);box-shadow:0 20px 25px -5px #0000001a,0 40px 100px -20px #0003}.gallery-content{grid-template-columns:1.2fr 1fr;height:600px;display:grid}@media (max-width:900px){.gallery-frame{flex:0 0 90vw}.gallery-content{grid-template-columns:1fr;height:auto;min-height:600px}}.gallery-visual{background:var(--color-cream);border-right:1px solid var(--color-mist);padding:var(--spacing-lg);justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden}.gallery-info{padding:var(--spacing-xl);flex-direction:column;display:flex;overflow-y:auto}.gallery-header{margin-bottom:var(--spacing-lg)}.gallery-title{font-family:var(--font-display);margin:0 0 var(--spacing-xs);color:var(--color-ink);font-size:2.5rem;font-style:italic}.gallery-meta{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.1em;color:var(--color-ash);font-size:.75rem}.gallery-desc{color:var(--color-charcoal);margin-bottom:var(--spacing-xl);max-width:45ch;font-size:1.125rem;line-height:1.6}.gallery-tags{gap:var(--spacing-xs);flex-wrap:wrap;margin-top:auto;display:flex}.gallery-tag{border:1px solid var(--color-mist);color:var(--color-ash);border-radius:4px;padding:6px 12px;font-size:.8125rem}.gallery-map{margin-top:var(--spacing-lg);justify-content:center;gap:8px;display:flex}.gallery-dot{background:var(--color-mist);cursor:pointer;width:40px;height:2px;font:inherit;border:none;padding:0;transition:all .3s;position:relative}.gallery-dot:after{content:"";position:absolute;inset:-10px 0}.gallery-dot:focus-visible{outline:2px solid var(--color-accent);outline-offset:4px;border-radius:1px}.gallery-dot.active{background:var(--color-accent);height:4px}.demo-tabbed-container{flex-direction:column;display:flex}.demo-tabs{background:var(--color-paper);border-bottom:1px solid var(--color-mist);justify-content:center;gap:0;margin-bottom:0;display:flex}.demo-tab{padding:var(--spacing-sm) var(--spacing-lg);font-family:var(--font-mono);letter-spacing:.05em;text-transform:uppercase;color:var(--color-ash);cursor:pointer;transition:all var(--duration-fast) var(--ease-out);background:0 0;border:none;border-bottom:2px solid #0000;font-size:.75rem;font-weight:500}.demo-tab:hover{color:var(--color-text);background:var(--color-cream)}.demo-tab.active{color:var(--color-accent);border-bottom-color:var(--color-accent);background:var(--color-accent-dim)}.demo-panels{flex:1}.demo-panel{display:none}.demo-panel.active{animation:fadeSlideIn .3s var(--ease-out);display:block}@keyframes fadeSlideIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.demo-container{background:var(--color-paper);border:none;border-radius:0;overflow:hidden}.demo-header{padding:var(--spacing-sm) var(--spacing-md);background:var(--color-paper);border-bottom:1px solid var(--color-mist);justify-content:center;align-items:center;min-height:48px;display:flex}.demo-toggle{align-items:center;gap:var(--spacing-md);display:flex}.demo-toggle-label{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;color:var(--color-ash);transition:color var(--duration-fast) var(--ease-out);cursor:pointer;font-size:.6875rem;font-weight:600}.demo-toggle-label:hover{color:var(--color-text)}.demo-toggle-label.active{color:var(--color-accent)}.demo-toggle-switch{background:var(--color-mist);cursor:pointer;width:44px;height:24px;transition:background var(--duration-fast) var(--ease-out);font:inherit;border:1px solid #0000;border-radius:12px;padding:0;position:relative}.demo-toggle-switch:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}.demo-toggle-switch:hover{border-color:var(--color-ash)}.demo-toggle-switch:after{content:"";background:var(--color-paper);width:16px;height:16px;transition:transform var(--duration-base) var(--ease-spring);border-radius:50%;position:absolute;top:3px;left:3px;box-shadow:0 1px 4px #00000026}.demo-toggle-switch.active{background:var(--color-accent)}.demo-toggle-switch.active:after{transform:translate(20px)}.demo-viewport{padding:var(--spacing-xl);background:var(--color-cream);min-height:280px;transition:background var(--duration-base) var(--ease-out);justify-content:center;align-items:center;display:flex}.demo-viewport[data-state=after]{background:var(--color-paper)}.demo-caption{padding:var(--spacing-sm) var(--spacing-md);font-family:var(--font-mono);letter-spacing:.03em;color:var(--color-ash);background:var(--color-paper);text-align:center;font-size:.6875rem}.uxw-demo{width:100%;max-width:320px;padding:var(--spacing-lg);background:var(--color-paper);border:1px solid var(--color-mist);text-align:center;border-radius:6px}.uxw-error-icon{margin-bottom:var(--spacing-sm);font-size:2rem}.uxw-error-title{color:#c00;margin-bottom:var(--spacing-xs);font-weight:600}.uxw-error-text{color:var(--color-ash);font-size:.875rem}.uxw-error-action{margin-top:var(--spacing-sm);color:var(--color-accent);cursor:pointer;font-size:.875rem;text-decoration:underline}.uxw-error-after .uxw-error-icon{color:var(--color-accent)}.uxw-error-after .uxw-error-title{color:var(--color-text)}.uxw-error-after .uxw-error-text{color:var(--color-charcoal)}.uxw-button-context{color:var(--color-charcoal);margin-bottom:var(--spacing-md);font-size:.875rem;font-weight:500}.uxw-button-row{gap:var(--spacing-sm);justify-content:center;display:flex}.uxw-btn{padding:var(--spacing-xs) var(--spacing-md);cursor:pointer;border:none;border-radius:4px;font-size:.875rem;font-weight:500}.uxw-btn-primary{background:var(--color-text);color:var(--color-paper)}.uxw-btn-secondary{color:var(--color-ash);border:1px solid var(--color-mist);background:0 0}.uxw-btn-danger{color:#fff;background:#c00}.uxw-empty-icon{margin-bottom:var(--spacing-sm);opacity:.4;font-size:2.5rem}.uxw-empty-title{color:var(--color-ash);font-weight:500}.uxw-empty-text{color:var(--color-charcoal);margin-top:var(--spacing-xs);font-size:.875rem}.uxw-empty-action{margin-top:var(--spacing-md)}.uxw-empty-after .uxw-empty-icon{opacity:1}.uxw-empty-after .uxw-empty-title{color:var(--color-text)}.spatial-demo{width:100%;max-width:340px;padding:var(--spacing-md);background:var(--color-paper);border:1px solid var(--color-mist);border-radius:6px}.spatial-grid-before{flex-wrap:wrap;gap:6px;display:flex}.spatial-grid-after{gap:var(--spacing-sm);grid-template-columns:1fr 1fr;display:grid}.spatial-card-item{padding:var(--spacing-sm);background:var(--color-bg);border:1px solid var(--color-mist);color:var(--color-charcoal);text-align:center;border-radius:4px;font-size:.8125rem}.spatial-grid-after .spatial-card-item{width:auto!important}.spatial-hierarchy-before .spatial-h-title,.spatial-hierarchy-before .spatial-h-subtitle,.spatial-hierarchy-before .spatial-h-cta,.spatial-hierarchy-before .spatial-h-link{margin-bottom:var(--spacing-xs);color:var(--color-charcoal);font-size:.9375rem}.spatial-hierarchy-after .spatial-h-title{font-family:var(--font-display);margin-bottom:var(--spacing-xs);color:var(--color-text);font-size:1.75rem;font-style:italic;font-weight:300}.spatial-hierarchy-after .spatial-h-subtitle{text-transform:uppercase;letter-spacing:.1em;color:var(--color-ash);margin-bottom:var(--spacing-md);font-size:.6875rem}.spatial-hierarchy-after .spatial-h-cta{padding:var(--spacing-sm) var(--spacing-lg);background:var(--color-text);color:var(--color-paper);margin-bottom:var(--spacing-sm);border-radius:4px;font-size:.875rem;font-weight:500;display:inline-block}.spatial-hierarchy-after .spatial-h-link{color:var(--color-ash);font-size:.75rem}.spatial-whitespace-before{padding:var(--spacing-xs)!important}.spatial-whitespace-before .spatial-ws-title{margin-bottom:2px;font-size:1rem;font-weight:600}.spatial-whitespace-before .spatial-ws-price{color:var(--color-ash);margin-bottom:4px;font-size:.875rem}.spatial-whitespace-before .spatial-ws-features{color:var(--color-ash);margin-bottom:6px;font-size:.75rem}.spatial-whitespace-before .spatial-ws-btn{background:var(--color-text);width:100%;color:var(--color-paper);cursor:pointer;border:none;border-radius:3px;padding:6px;font-size:.75rem}.spatial-whitespace-after{padding:var(--spacing-lg)!important}.spatial-whitespace-after .spatial-ws-title{font-family:var(--font-display);margin-bottom:var(--spacing-sm);font-size:1.5rem;font-weight:400}.spatial-whitespace-after .spatial-ws-price{color:var(--color-text);margin-bottom:var(--spacing-sm);font-size:1.25rem;font-weight:600}.spatial-whitespace-after .spatial-ws-features{color:var(--color-ash);margin-bottom:var(--spacing-lg);font-size:.8125rem;line-height:1.6}.spatial-whitespace-after .spatial-ws-btn{width:100%;padding:var(--spacing-sm);background:var(--color-text);color:var(--color-paper);cursor:pointer;border:none;border-radius:4px;font-size:.875rem;font-weight:500}.motion-demo{align-items:center;gap:var(--spacing-sm);flex-direction:column;width:100%;max-width:280px;display:flex}.motion-stagger-demo{align-items:stretch}.motion-list-item{align-items:center;gap:var(--spacing-sm);padding:var(--spacing-sm) var(--spacing-md);background:var(--color-bg);border:1px solid var(--color-mist);color:var(--color-charcoal);border-radius:4px;font-size:.875rem;display:flex}.motion-dot{background:var(--color-accent);border-radius:50%;width:8px;height:8px}.demo-viewport[data-state=after] .motion-list-item{opacity:0;animation:.35s cubic-bezier(.16,1,.3,1) forwards staggerIn;transform:translateY(12px)}.demo-viewport[data-state=after] .motion-list-item:first-child{animation-delay:0s}.demo-viewport[data-state=after] .motion-list-item:nth-child(2){animation-delay:50ms}.demo-viewport[data-state=after] .motion-list-item:nth-child(3){animation-delay:.1s}.demo-viewport[data-state=after] .motion-list-item:nth-child(4){animation-delay:.15s}@keyframes staggerIn{to{opacity:1;transform:translateY(0)}}.motion-btn{cursor:pointer;border:none;border-radius:4px;padding:12px 24px;font-size:.9375rem;font-weight:500}.motion-btn-before{background:var(--color-charcoal);color:var(--color-paper)}.motion-btn-after{background:var(--color-text);color:var(--color-paper);transition:transform .2s cubic-bezier(.34,1.56,.64,1),box-shadow .2s}.motion-btn-after:hover{transform:translateY(-2px);box-shadow:0 4px 12px #00000026}.motion-btn-after:active{transform:translateY(0)scale(.98)}.motion-card{padding:var(--spacing-md);background:var(--color-bg);border:1px solid var(--color-mist);text-align:center;border-radius:6px;min-width:140px}.motion-card-icon{margin-bottom:var(--spacing-xs);font-size:1.5rem}.motion-card-text{color:var(--color-charcoal);font-size:.8125rem}.motion-card-after{transition:all .3s cubic-bezier(.34,1.56,.64,1)}.demo-viewport[data-state=after] .motion-card-after{background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.demo-viewport[data-state=after] .motion-card-after{background:color-mix(in oklch, var(--color-accent) 10%, var(--color-paper))}}.demo-viewport[data-state=after] .motion-card-after{border-color:var(--color-accent)}.demo-viewport[data-state=after] .motion-card-after .motion-card-icon{animation:.4s cubic-bezier(.34,1.56,.64,1) checkPop}@keyframes checkPop{50%{transform:scale(1.3)}}.typo-demo{text-align:left;width:100%;max-width:320px}.typo-pairing-before{font-family:Inter,system-ui,sans-serif}.typo-pairing-before .typo-heading{margin-bottom:var(--spacing-xs);font-size:1.5rem;font-weight:600}.typo-pairing-before .typo-body{color:var(--color-ash);font-size:.9375rem;line-height:1.5}.typo-pairing-after .typo-heading{font-family:var(--font-display);letter-spacing:-.02em;margin-bottom:var(--spacing-sm);color:var(--color-text);font-size:2rem;font-style:italic;font-weight:300}.typo-pairing-after .typo-body{font-family:var(--font-body);color:var(--color-charcoal);font-size:.9375rem;line-height:1.7}.typo-hierarchy-before .typo-h1{margin-bottom:4px;font-size:1.125rem;font-weight:600}.typo-hierarchy-before .typo-meta{color:var(--color-ash);margin-bottom:var(--spacing-xs);font-size:.9375rem}.typo-hierarchy-before .typo-p{color:var(--color-charcoal);font-size:.875rem;line-height:1.5}.typo-hierarchy-after .typo-h1{font-family:var(--font-display);letter-spacing:-.03em;margin-bottom:2px;font-size:2.25rem;font-weight:300;line-height:1.1}.typo-hierarchy-after .typo-meta{text-transform:uppercase;letter-spacing:.12em;color:var(--color-accent);margin-bottom:var(--spacing-md);font-size:.6875rem}.typo-hierarchy-after .typo-p{color:var(--color-ash);font-size:.9375rem;line-height:1.7}.int-demo{gap:var(--spacing-md);flex-direction:column;width:100%;max-width:280px;display:flex}.int-states-demo{gap:var(--spacing-lg)}.int-state-row{align-items:center;gap:var(--spacing-md);display:flex}.int-state-label{text-transform:uppercase;letter-spacing:.08em;color:var(--color-ash);width:40px;font-size:.6875rem}.int-btn{padding:var(--spacing-sm) var(--spacing-md);cursor:pointer;border-radius:4px;flex:1;font-size:.875rem;font-weight:500}.int-btn-poor{background:var(--color-charcoal);color:var(--color-paper);border:none}.int-btn-good{background:var(--color-text);color:var(--color-paper);border:2px solid #0000;transition:all .15s}.int-btn-good:hover{background:var(--color-charcoal)}.int-btn-good:focus{border-color:var(--color-accent);box-shadow:0 0 0 3px var(--color-accent);outline:none}@supports (color:color-mix(in lab, red, red)){.int-btn-good:focus{box-shadow:0 0 0 3px color-mix(in oklch, var(--color-accent) 25%, transparent)}}.int-btn-good:active{transform:scale(.98)}.int-aff-item{padding:var(--spacing-sm) var(--spacing-md);cursor:pointer;border-radius:4px;font-size:.875rem}.int-aff-poor{color:var(--color-charcoal)}.int-aff-good{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.int-aff-good:after{content:" →"}.int-affordance-after .int-aff-item{background:var(--color-bg);border:1px solid var(--color-mist);color:var(--color-accent);text-underline-offset:2px;text-decoration:underline;transition:background .15s}.int-affordance-after .int-aff-item:hover{background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.int-affordance-after .int-aff-item:hover{background:color-mix(in oklch, var(--color-accent) 5%, var(--color-paper))}}.int-affordance-after .int-aff-item:after{content:" →"}.int-feedback-before,.int-feedback-after{align-items:center;gap:var(--spacing-md);flex-direction:row;display:flex}.int-fb-btn{cursor:pointer;border:none;border-radius:50%;justify-content:center;align-items:center;width:48px;height:48px;display:flex}.int-fb-btn svg{width:22px;height:22px}.int-fb-silent{background:var(--color-mist);color:var(--color-ash)}.int-fb-active{background:var(--color-charcoal);color:var(--color-paper);transition:all .15s cubic-bezier(.34,1.56,.64,1)}.int-fb-active:hover{transform:scale(1.1)}.int-fb-active:active{transform:scale(.95)}.int-fb-active.liked{background:var(--color-accent);animation:.35s cubic-bezier(.34,1.56,.64,1) heartPop}@keyframes heartPop{50%{transform:scale(1.25)}}.int-fb-label{color:var(--color-charcoal);font-size:.875rem}.color-demo{width:100%;max-width:300px}.color-palette-before,.color-palette-after{gap:var(--spacing-xs);padding:var(--spacing-md);background:var(--color-paper);border:1px solid var(--color-mist);border-radius:6px;flex-wrap:wrap;display:flex}.color-swatch{border-radius:4px;width:40px;height:40px;transition:background .2s}.color-card{width:100%;margin-top:var(--spacing-sm);padding:var(--spacing-sm);background:var(--color-paper);border:1px solid var(--color-mist);border-radius:4px;flex-direction:column;gap:4px;display:flex}.color-card span{font-size:.8125rem;font-weight:500;transition:color .2s}.color-card button{cursor:pointer;border:none;border-radius:3px;padding:6px;font-size:.75rem;font-weight:500;transition:all .2s}.color-palette-before .swatch-1{background:#ff6b6b}.color-palette-before .swatch-2{background:#4ecdc4}.color-palette-before .swatch-3{background:#ffe66d}.color-palette-before .swatch-4{background:#95e1d3}.color-palette-before .swatch-5{background:#f38181}.color-palette-before .card-title{color:#ff6b6b}.color-palette-before .card-subtitle{color:#4ecdc4}.color-palette-before .card-btn{color:#333;background:#ffe66d}.color-palette-after .swatch-1{background:var(--color-text)}.color-palette-after .swatch-2{background:var(--color-charcoal)}.color-palette-after .swatch-3{background:var(--color-ash)}.color-palette-after .swatch-4{background:var(--color-mist)}.color-palette-after .swatch-5{background:var(--color-accent)}.color-palette-after .card-title{color:var(--color-text)}.color-palette-after .card-subtitle{color:var(--color-ash)}.color-palette-after .card-btn{background:var(--color-accent);color:var(--color-paper)}.color-accent-card{padding:var(--spacing-md);border-radius:6px}.color-accent-before .color-accent-card{background:#f5f5f5;border:1px solid #e0e0e0}.color-accent-before .color-accent-title{color:#333;margin-bottom:4px;font-weight:600}.color-accent-before .color-accent-text{color:#666;margin-bottom:var(--spacing-sm);font-size:.8125rem}.color-accent-before .color-accent-btn{width:100%;padding:var(--spacing-xs);color:#fff;cursor:pointer;background:#333;border:none;border-radius:4px;font-size:.8125rem}.color-accent-after .color-accent-card{background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.color-accent-after .color-accent-card{background:color-mix(in oklch, var(--color-accent) 8%, var(--color-paper))}}.color-accent-after .color-accent-card{border:1px solid var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.color-accent-after .color-accent-card{border:1px solid color-mix(in oklch, var(--color-accent) 20%, var(--color-paper))}}.color-accent-after .color-accent-title{color:var(--color-text);margin-bottom:4px;font-weight:600}.color-accent-after .color-accent-text{color:var(--color-ash);margin-bottom:var(--spacing-sm);font-size:.8125rem}.color-accent-after .color-accent-btn{width:100%;padding:var(--spacing-xs);background:var(--color-accent);color:var(--color-paper);cursor:pointer;border:none;border-radius:4px;font-size:.8125rem;font-weight:500}.color-contrast-static{gap:var(--spacing-sm);flex-direction:column;display:flex}.contrast-example{padding:var(--spacing-md);text-align:center;border-radius:6px}.contrast-fail{color:#a0a0a0;background:#f0f0f0}.contrast-pass{background:var(--color-charcoal);color:var(--color-paper)}.contrast-badge{text-transform:uppercase;letter-spacing:.1em;border-radius:2px;margin-bottom:4px;padding:2px 6px;font-size:.5625rem;font-weight:600;display:inline-block}.contrast-fail .contrast-badge{background:#ddd}.contrast-pass .contrast-badge{background:var(--color-accent);color:var(--color-paper)}.contrast-text{margin-bottom:2px;font-size:1rem;font-weight:500}.contrast-ratio{opacity:.7;font-size:.6875rem}.resp-demo{width:100%;max-width:340px}.resp-touch-demo{gap:var(--spacing-lg);flex-direction:column;display:flex}.resp-touch-row{align-items:center;gap:var(--spacing-md);display:flex}.resp-label{text-transform:uppercase;letter-spacing:.08em;color:var(--color-ash);width:70px;font-size:.6875rem}.resp-touch-targets{gap:4px;display:flex}.resp-touch-targets button{cursor:pointer;border:none;border-radius:4px;font-weight:500}.resp-touch-bad button{background:var(--color-mist);width:24px;height:24px;color:var(--color-ash);font-size:.75rem}.resp-touch-good button{background:var(--color-text);width:44px;height:44px;color:var(--color-paper);font-size:1rem}.resp-fluid-demo{padding:var(--spacing-md);background:var(--color-bg);border:1px solid var(--color-mist);border-radius:6px}.resp-fluid-container{gap:var(--spacing-md);flex-direction:column;display:flex}.resp-fluid-fixed,.resp-fluid-adaptive{color:var(--color-ash);font-size:.75rem}.resp-fluid-fixed span,.resp-fluid-adaptive span{margin-bottom:4px;display:block}.resp-fluid-bar{background:var(--color-mist);border-radius:4px;height:24px}.resp-fluid-adaptive .resp-fluid-bar{background:var(--color-accent)}.resp-adapt-demo{gap:var(--spacing-sm);align-items:flex-end;display:flex}.resp-device{text-align:center}.resp-device>span{color:var(--color-ash);text-transform:uppercase;letter-spacing:.08em;margin-top:4px;font-size:.625rem;display:block}.resp-device-screen{background:var(--color-paper);border:2px solid var(--color-mist);border-radius:4px;flex-direction:column;gap:3px;padding:4px;display:flex}.resp-device-mobile .resp-device-screen{width:50px;height:80px}.resp-device-tablet .resp-device-screen{width:80px;height:60px}.resp-device-desktop .resp-device-screen{width:120px;height:70px}.resp-block{background:var(--color-mist);border-radius:2px}.resp-block-row{flex:1;gap:3px;display:flex}.resp-header{background:var(--color-charcoal);height:16px}.resp-sidebar{background:var(--color-charcoal);width:30%}.resp-content{flex:1}@keyframes fadeIn{to{opacity:1}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}*,:before,:after{box-sizing:border-box}*{margin:0}img,picture,video,canvas,svg{max-width:100%;display:block}button,input,textarea,select{font:inherit}:root{--font-display:"Cormorant Garamond", Georgia, serif;--font-body:"Instrument Sans", system-ui, sans-serif;--font-mono:"Space Grotesk", monospace;--spacing-xs:8px;--spacing-sm:16px;--spacing-md:24px;--spacing-lg:32px;--spacing-xl:48px;--spacing-2xl:80px;--spacing-3xl:120px;--width-max:1400px;--width-content:900px;--ease-out:cubic-bezier(.16, 1, .3, 1);--ease-in-out:cubic-bezier(.65, 0, .35, 1);--ease-spring:cubic-bezier(.34, 1.56, .64, 1);--duration-fast:.15s;--duration-base:.3s;--duration-slow:.6s;--duration-slower:.8s;--duration-slowest:1.2s;--color-ink:oklch(10% 0 0);--color-text:oklch(10% 0 0);--color-paper:oklch(98% 0 0);--color-cream:oklch(96% .005 350);--color-charcoal:oklch(25% 0 0);--color-ash:oklch(55% 0 0);--color-mist:oklch(92% 0 0);--color-bg:oklch(96% .005 350);--color-accent:oklch(60% .25 350);--color-accent-hover:oklch(52% .25 350);--color-accent-dim:oklch(60% .25 350/.15);--color-accent-soft:oklch(60% .25 350/.25);--cat-diagnostic-bg:#fdf4ff;--cat-diagnostic-border:#d946ef;--cat-diagnostic-text:#a21caf;--cat-quality-bg:#f0fdf4;--cat-quality-border:#22c55e;--cat-quality-text:#15803d;--cat-intensity-bg:#fffbeb;--cat-intensity-border:#f59e0b;--cat-intensity-text:#b45309;--cat-adaptation-bg:#eff6ff;--cat-adaptation-border:#3b82f6;--cat-adaptation-text:#1d4ed8;--cat-enhancement-bg:#fdf2f8;--cat-enhancement-border:#ec4899;--cat-enhancement-text:#be185d;--cat-system-bg:#f5f5f4;--cat-system-border:#78716c;--cat-system-text:#44403c}.skip-link{z-index:10000;padding:var(--spacing-sm) var(--spacing-lg);background:var(--color-ink);color:var(--color-paper);border-radius:0 0 8px 8px;font-weight:600;text-decoration:none;transition:top .2s;position:absolute;top:-100%;left:50%;transform:translate(-50%)}.skip-link:focus{outline:2px solid var(--color-accent);outline-offset:2px;top:0}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;overflow-x:clip}body{font-family:var(--font-body);color:var(--color-text);background:var(--color-paper);min-height:100dvh;font-size:16px;line-height:1.625;overflow-x:clip}h1,h2,h3,h4,h5,h6{font-family:var(--font-display);letter-spacing:-.02em;color:var(--color-ink);font-weight:400;line-height:1.1}a{color:var(--color-accent);text-underline-offset:2px;transition:color var(--duration-fast) var(--ease-out), text-decoration-color var(--duration-fast) var(--ease-out);text-decoration:underline;text-decoration-thickness:1px}a:hover{color:var(--color-accent-hover);text-decoration-thickness:2px}.btn,.footer-logo,[class*=nav-item]{text-decoration:none}strong{color:var(--color-ink);font-weight:600}code{font-family:var(--font-mono);background:var(--color-accent-dim);color:var(--color-accent);border-radius:4px;padding:.15em .4em;font-size:.9em}::selection{background:var(--color-accent-soft);color:var(--color-ink)}.grain-overlay{pointer-events:none;z-index:9999;opacity:.03;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");background-repeat:repeat;position:fixed;inset:0}.site-content{max-width:var(--width-max);padding:0 var(--spacing-lg);margin:0 auto}@media (max-width:768px){.site-content{padding:0 var(--spacing-md)}}.section-header{margin-bottom:var(--spacing-lg);position:relative}.section-number{font-family:var(--font-mono);letter-spacing:.05em;color:var(--color-ash);margin-bottom:var(--spacing-xs);text-transform:uppercase;font-size:.625rem;font-weight:500;display:block}.section-title{margin:0;font-size:clamp(1.75rem,4vw,2.5rem);font-weight:400;line-height:1.2}.section-subtitle{color:var(--color-charcoal);margin-top:var(--spacing-sm);max-width:55ch;font-size:1rem;line-height:1.6}.cheatsheet-link{color:var(--color-accent);margin-left:.5em;font-size:.875rem;text-decoration:none}.cheatsheet-link:hover{text-decoration:underline}.section-lead{color:var(--color-charcoal);max-width:55ch;margin-bottom:var(--spacing-lg);font-size:1rem;line-height:1.6}.hero-combined{min-height:100dvh;padding:var(--spacing-2xl) 0;background:var(--color-paper);flex-direction:column;justify-content:center;display:flex;position:relative}.github-link{top:var(--spacing-md);right:var(--spacing-lg);z-index:10;color:var(--color-ash);transition:color .2s;position:absolute}.github-link:hover{color:var(--color-ink)}.github-link svg{width:28px;height:28px}.hero-combined-container{max-width:var(--width-max);padding:0 var(--spacing-lg);gap:var(--spacing-xl);grid-template-columns:1fr 1fr;align-items:center;width:100%;margin:0 auto;display:grid}@media (max-width:1024px){.hero-combined-container{gap:var(--spacing-lg);text-align:center;grid-template-columns:1fr}}.hero-combined-left{gap:var(--spacing-md);flex-direction:column;display:flex}@media (max-width:1024px){.hero-combined-left{align-items:center}}.hero-title-combined{font-family:var(--font-display);letter-spacing:-.02em;color:var(--color-ink);margin:0;font-size:clamp(2.5rem,7vw,4.5rem);font-style:italic;font-weight:300;line-height:1}.hero-tagline-combined{font-family:var(--font-display);color:var(--color-charcoal);margin:0;font-size:clamp(1.125rem,2.5vw,1.75rem);font-style:italic;font-weight:400;line-height:1.3}.hero-hook-text{color:var(--color-charcoal);max-width:45ch;margin:0;font-size:1rem;line-height:1.6}.hero-included-box{border:1px solid var(--color-mist);background:0 0;flex-direction:column;gap:6px;max-width:45ch;padding:10px 14px;display:flex}.hero-included-title{font-family:var(--font-body);text-transform:uppercase;letter-spacing:.1em;color:var(--color-ash);font-size:.5625rem;font-weight:500}.hero-included-items{color:var(--color-charcoal);flex-wrap:wrap;align-items:center;gap:6px;font-size:.8125rem;line-height:1.5;display:flex}.hero-included-items em{font-style:normal;font-family:var(--font-mono);font-size:.75rem}.hero-included-sep{color:var(--color-mist)}@media (max-width:500px){.hero-included-items{flex-direction:column;align-items:flex-start;gap:4px}.hero-included-sep{display:none}}.hero-cta-group{align-items:center;gap:var(--spacing-lg);margin-top:var(--spacing-sm);display:flex}@media (max-width:600px){.hero-cta-group{gap:var(--spacing-md);flex-direction:column}}.hero-cta-combined{padding:var(--spacing-sm) var(--spacing-xl);font-family:var(--font-body);letter-spacing:.05em;text-transform:uppercase;color:var(--color-paper);background:var(--color-ink);border:none;font-size:.9rem;font-weight:500;text-decoration:none;transition:transform .2s,background .2s;display:inline-block}.hero-cta-combined:hover{background:var(--color-accent);color:var(--color-paper);transform:translateY(-2px)}.hero-logos-inline{align-items:center;gap:var(--spacing-sm);display:flex}.hero-logos-inline .hero-logos-label{color:var(--color-ash);letter-spacing:.03em;font-size:.6875rem}.hero-logos-inline .hero-logos-row{align-items:center;gap:8px;display:flex}.hero-logos-inline .hero-logos-row img{opacity:.7;border-radius:4px;transition:opacity .2s}.hero-logos-inline .hero-logos-row img:hover{opacity:1}.hero-combined-right{justify-content:center;display:flex}.hero-combined-right .split-comparison{width:100%;max-width:520px}.hero-combined-right .split-container{max-width:100%}.hero-bias-tags{align-items:center;gap:var(--spacing-xs);margin-top:var(--spacing-lg);padding-top:var(--spacing-md);border-top:1px solid var(--color-mist);max-width:var(--width-max);width:100%;padding-bottom:var(--spacing-md);flex-direction:column;margin-left:auto;margin-right:auto;display:flex}.problem-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist)}.problem-content{gap:var(--spacing-xl);display:grid}.split-comparison{width:100%;max-width:600px;margin:-20px auto;padding:20px;position:relative}.split-container{background:var(--color-cream);border:1px solid var(--color-mist);cursor:ew-resize;-webkit-user-select:none;user-select:none;border-radius:12px;width:100%;max-width:500px;height:380px;margin:0 auto;position:relative;overflow:hidden}.split-before,.split-after{justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.split-before{z-index:1}.split-content{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.split-after{clip-path:polygon(78% 0%,100% 0%,100% 100%,62% 100%);z-index:2;background:var(--color-paper)}.split-divider{background:var(--color-accent);pointer-events:none;z-index:3;width:3px;position:absolute;top:0;bottom:0;left:70%;transform:translate(-50%)skew(-10deg);box-shadow:0 0 20px #00000026}.split-label{letter-spacing:.08em;text-transform:uppercase;color:var(--color-paper);background:var(--color-accent);white-space:nowrap;border-radius:4px;padding:6px 14px;font-size:.6875rem;font-weight:600;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)skew(10deg);box-shadow:0 2px 8px #0003}.slop-card{background:linear-gradient(135deg,#f5f3ff 0%,#ede9fe 50%,#ddd6fe 100%);border-radius:16px;flex-direction:column;width:280px;height:280px;padding:24px;font-family:Inter,system-ui,sans-serif;display:flex;box-shadow:0 4px 6px -1px #0000001a}.slop-header{align-items:center;gap:12px;margin-bottom:16px;display:flex}.slop-avatar{background:linear-gradient(135deg,#8b5cf6,#7c3aed);border-radius:50%;flex-shrink:0;width:40px;height:40px}.slop-text{flex:1}.slop-title{color:#1f2937;margin-bottom:2px;font-size:14px;font-weight:600}.slop-subtitle{color:#6b7280;font-size:12px}.slop-body{color:#4b5563;flex:1;margin-bottom:auto;font-size:13px;line-height:1.5}.slop-button{color:#fff;cursor:pointer;background:linear-gradient(135deg,#8b5cf6,#7c3aed);border:none;border-radius:8px;width:100%;margin-top:auto;padding:10px 20px;font-family:Inter,system-ui,sans-serif;font-size:13px;font-weight:500}.slop-callouts{pointer-events:none;position:absolute;inset:0}.slop-callout{text-transform:uppercase;letter-spacing:.06em;color:var(--color-accent);background:var(--color-paper);border:1px solid var(--color-accent);white-space:nowrap;opacity:0;animation:calloutFadeIn .4s var(--ease-out) forwards;border-radius:3px;padding:4px 8px;font-size:.625rem;font-weight:600;position:absolute;box-shadow:0 2px 8px #0000001a}.slop-callout[data-point=font]{animation-delay:.1s;top:15%;right:5%}.slop-callout[data-point=gradient]{animation-delay:.25s;top:40%;left:5%}.slop-callout[data-point=copy]{animation-delay:.4s;bottom:35%;right:8%}.slop-callout[data-point=rounded]{animation-delay:.55s;bottom:12%;left:10%}@keyframes calloutFadeIn{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}.impeccable-card{background:var(--color-paper);border:1px solid var(--color-mist);width:280px;height:300px;padding:var(--spacing-lg);text-align:left;flex-direction:column;display:flex}.impeccable-eyebrow{font-family:var(--font-mono);letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:var(--spacing-xs);font-size:.625rem;font-weight:500}.impeccable-title{font-family:var(--font-display);color:var(--color-ink);margin-bottom:var(--spacing-sm);font-size:1.75rem;font-style:italic;font-weight:300;line-height:1.1}.impeccable-body{color:var(--color-ash);flex:1;margin-bottom:auto;font-size:.875rem;line-height:1.6}.impeccable-button{margin-top:var(--spacing-sm);background:var(--color-ink);color:var(--color-paper);font-family:var(--font-body);letter-spacing:.03em;cursor:pointer;transition:all var(--duration-base) var(--ease-out);border:none;align-self:flex-start;padding:.625rem 1.5rem;font-size:.8125rem;font-weight:500;display:inline-flex}.impeccable-button:hover{background:var(--color-accent)}.split-labels{justify-content:center;gap:var(--spacing-xl);margin-top:var(--spacing-md);display:flex}.split-label-item{align-items:center;gap:var(--spacing-xs);color:var(--color-ash);font-size:.8125rem;display:flex}.split-label-dot{background:var(--color-mist);border-radius:50%;width:8px;height:8px}.split-label-dot--accent{background:var(--color-accent)}.solution-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist)}.solution-content{gap:var(--spacing-lg);display:grid}.solution-content .section-lead{margin-bottom:0}.solution-visual{gap:var(--spacing-lg);grid-template-columns:1fr auto 1fr;align-items:stretch;display:grid}@media (max-width:900px){.solution-visual{gap:var(--spacing-md);grid-template-columns:1fr}}.solution-visual-interactive{background:var(--color-paper);border:1px solid var(--color-mist);border-radius:8px;width:100%;min-height:380px;position:relative;overflow:hidden}.solution-pillar{background:var(--color-cream);border:1px solid var(--color-mist);padding:var(--spacing-lg);transition:all var(--duration-base) var(--ease-out)}.solution-pillar:hover{border-color:var(--color-accent);box-shadow:0 20px 60px var(--color-accent-dim);transform:translateY(-4px)}.pillar-header{text-align:center;margin-bottom:var(--spacing-lg);padding-bottom:var(--spacing-md);border-bottom:1px solid var(--color-mist)}.pillar-icon{background:var(--color-accent-dim);width:56px;height:56px;color:var(--color-accent);margin-bottom:var(--spacing-sm);border-radius:50%;justify-content:center;align-items:center;display:inline-flex}.pillar-title{font-family:var(--font-display);margin:0 0 var(--spacing-xs);font-size:1.75rem;font-weight:400}.pillar-subtitle{color:var(--color-ash);margin:0;font-size:.875rem}.pillar-content{gap:var(--spacing-sm);flex-direction:column;display:flex}.pillar-item{padding:var(--spacing-sm);background:var(--color-paper);transition:all var(--duration-fast) var(--ease-out);border-radius:4px;justify-content:space-between;align-items:center;display:flex}.pillar-item:hover{background:var(--color-accent-dim)}.pillar-item-name{color:var(--color-ink);font-size:.9375rem;font-weight:500}.pillar-item-code{font-family:var(--font-mono);color:var(--color-accent);background:0 0;padding:0;font-size:.875rem;font-weight:500}.pillar-item-desc{color:var(--color-ash);font-size:.75rem}.pillar-item--more{color:var(--color-accent);border:1px dashed var(--color-mist);background:0 0;justify-content:center;font-size:.8125rem;font-weight:500}.solution-connector{justify-content:center;align-items:center;display:flex}.connector-plus{font-family:var(--font-display);color:var(--color-accent);opacity:.5;font-size:3rem;font-weight:300}@media (max-width:900px){.solution-connector{padding:var(--spacing-sm) 0}.connector-plus{font-size:2rem}}.skills-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist)}.skills-gallery{gap:var(--spacing-xl);grid-template-columns:200px 1fr;align-items:start;display:grid}@media (max-width:968px){.skills-gallery{gap:var(--spacing-lg);grid-template-columns:1fr}}.skills-nav{top:var(--spacing-lg);flex-direction:column;gap:2px;display:flex;position:sticky}@media (max-width:968px){.skills-nav{gap:var(--spacing-xs);flex-flow:wrap;position:static}}.skill-nav-item{padding:var(--spacing-sm) var(--spacing-md);color:var(--color-ash);font-family:var(--font-body);cursor:pointer;text-align:left;background:0 0;border:none;border-left:2px solid #0000;font-size:.9375rem;font-weight:400;text-decoration:none;transition:all .2s;display:block}.skill-nav-item:hover{color:var(--color-text);background:var(--color-cream)}.skill-nav-item.active{color:var(--color-accent);border-left-color:var(--color-accent);background:var(--color-accent-dim);font-weight:500}@media (max-width:968px){.skill-nav-item{padding:var(--spacing-xs) var(--spacing-md);border-bottom:2px solid #0000;border-left:none}.skill-nav-item.active{border-bottom-color:var(--color-accent)}}.skills-showcase{gap:var(--spacing-lg);grid-template-columns:1.2fr 1fr;align-items:start;display:grid}@media (max-width:1100px){.skills-showcase{grid-template-columns:1fr}}.loading-state{padding:var(--spacing-xl);text-align:center;color:var(--color-ash);font-style:italic}.mobile-commands-layout{display:none}@media (max-width:900px){.mobile-commands-layout{gap:var(--spacing-md);flex-direction:column;display:flex}.commands-container{display:none}}.mobile-carousel-wrapper{-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:var(--spacing-xs) 0;overflow-x:auto}.mobile-carousel-wrapper::-webkit-scrollbar{display:none}.mobile-carousel{gap:var(--spacing-xs);padding-right:var(--spacing-md);display:flex}.mobile-cmd-pill{padding:var(--spacing-sm) var(--spacing-md);min-height:44px;font-family:var(--font-mono);color:var(--color-ash);background:var(--color-cream);border:1px solid var(--color-mist);cursor:pointer;white-space:nowrap;border-radius:100px;flex-shrink:0;font-size:.8125rem;font-weight:500;transition:all .2s}.mobile-cmd-pill:hover{color:var(--color-charcoal);border-color:var(--color-charcoal)}.mobile-cmd-pill.active{color:var(--color-paper);background:var(--color-ink);border-color:var(--color-ink)}.mobile-demo-area{background:var(--color-cream);border:1px solid var(--color-mist);padding:var(--spacing-sm);border-radius:8px}.mobile-demo-area .demo-split-comparison{width:100%}.mobile-demo-area .split-container{width:100%;max-width:100%;height:320px}.mobile-demo-area .demo-caption{margin-top:var(--spacing-sm);font-size:.75rem}.mobile-info-area{padding-top:var(--spacing-sm)}.mobile-cmd-info{padding:var(--spacing-sm) 0;display:none}.mobile-cmd-info.active{display:block}.mobile-cmd-name{font-family:var(--font-mono);color:var(--color-ink);margin:0 0 var(--spacing-xs) 0;font-size:1.125rem;font-weight:600}.mobile-cmd-desc{color:var(--color-charcoal);margin:0;font-size:.875rem;line-height:1.5}.mobile-cmd-rel{margin-top:var(--spacing-xs);color:var(--color-ash);font-size:.75rem}.mobile-cmd-rel code{font-family:var(--font-mono);color:var(--color-ink)}.downloads-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist)}.downloads-grid{gap:var(--spacing-lg);grid-template-columns:repeat(auto-fit,minmax(280px,1fr));display:grid}.download-card{text-align:center;padding:var(--spacing-lg);background:var(--color-cream);border:1px solid var(--color-mist);transition:all var(--duration-base) var(--ease-out);flex-direction:column;align-items:center;display:flex}.download-card:hover{border-color:var(--color-accent);box-shadow:0 20px 60px var(--color-accent-dim);transform:translateY(-4px)}.download-card-icon{margin-bottom:var(--spacing-sm)}.download-card-icon img{object-fit:contain;border-radius:8px;width:40px;height:40px}.download-card-title{font-family:var(--font-display);margin:0 0 var(--spacing-sm) 0;font-size:1.25rem;font-weight:400}.download-card-note{color:var(--color-ash);margin-bottom:var(--spacing-xs);font-size:.75rem}.download-card .btn{margin-top:var(--spacing-xs)}.install-command{align-items:center;gap:var(--spacing-xs);background:var(--color-paper);border:1px solid var(--color-mist);padding:var(--spacing-sm);margin-top:var(--spacing-sm);border-radius:6px;width:100%;display:flex}.install-command code{font-family:var(--font-mono);color:var(--color-ink);white-space:nowrap;text-overflow:ellipsis;background:0 0;flex:1;padding:0;font-size:.75rem;overflow:hidden}.copy-btn{border:1px solid var(--color-mist);width:28px;height:28px;color:var(--color-ash);cursor:pointer;transition:all var(--duration-fast) var(--ease-out);background:0 0;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.copy-btn:hover{background:var(--color-accent-dim);border-color:var(--color-accent);color:var(--color-accent)}.copy-btn.copied{background:var(--color-accent);border-color:var(--color-accent);color:var(--color-paper)}.install-hint{color:var(--color-ash);margin:var(--spacing-xs) 0 0 0;font-size:.75rem}.install-hint code{font-family:var(--font-mono);background:var(--color-mist);border-radius:3px;padding:2px 5px;font-size:.6875rem}.download-card-details{width:100%;margin-top:var(--spacing-sm);text-align:left;font-size:.8125rem}.download-card-details summary{cursor:pointer;color:var(--color-ash);padding:var(--spacing-xs) 0;justify-content:center;align-items:center;gap:4px;font-size:.75rem;list-style:none;display:flex}.download-card-details summary:before{content:"▶";transition:transform var(--duration-fast) var(--ease-out);font-size:.5rem}.download-card-details[open] summary:before{transform:rotate(90deg)}.download-card-details summary::-webkit-details-marker{display:none}.download-card-details ol{margin:var(--spacing-sm) 0;padding-left:var(--spacing-md);color:var(--color-charcoal);line-height:1.6}.download-card-details li{margin-bottom:4px}.download-card-details code{font-family:var(--font-mono);background:var(--color-mist);border-radius:3px;padding:2px 5px;font-size:.6875rem}.download-card-details a{color:var(--color-accent);font-size:.75rem;text-decoration:none}.download-card-details a:hover{text-decoration:underline}.opensource-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist);text-align:center}.opensource-content{align-items:center;gap:var(--spacing-md);flex-direction:column;max-width:500px;margin:0 auto;display:flex}.opensource-title{font-size:clamp(1.5rem,4vw,2rem);font-weight:300}.opensource-desc{color:var(--color-ash);font-size:1.125rem;line-height:1.6}.site-footer{border-top:1px solid var(--color-mist);padding:var(--spacing-xl) var(--spacing-lg);background:var(--color-cream)}.footer-content{max-width:var(--width-max);justify-content:space-between;align-items:center;gap:var(--spacing-lg);flex-wrap:wrap;margin:0 auto;display:flex}@media (max-width:768px){.footer-content{text-align:center;flex-direction:column}}.footer-brand{gap:var(--spacing-xs);flex-direction:column;display:flex}.footer-logo{font-family:var(--font-display);color:var(--color-ink);font-size:1.25rem;font-weight:400}.footer-tagline{color:var(--color-ash);font-size:.875rem}.footer-links{gap:var(--spacing-lg);flex-wrap:wrap;display:flex}.footer-links a{color:var(--color-ash);transition:color var(--duration-fast) var(--ease-out);font-size:.875rem}.footer-links a:hover{color:var(--color-accent)}.footer-author{justify-content:center;align-items:center;gap:var(--spacing-md);width:100%;padding-top:var(--spacing-lg);margin-top:var(--spacing-md);border-top:1px solid var(--color-mist);flex-wrap:wrap;display:flex}.footer-author-label{color:var(--color-ash);font-size:.875rem}.footer-author-label a{color:var(--color-text);transition:color var(--duration-fast) var(--ease-out)}.footer-author-label a:hover{color:var(--color-accent)}.footer-author-links{align-items:center;gap:var(--spacing-sm);display:flex}.footer-social-link{width:36px;height:36px;color:var(--color-ash);transition:all var(--duration-fast) var(--ease-out);background:0 0;border-radius:50%;justify-content:center;align-items:center;text-decoration:none;display:flex}.footer-social-link:hover{color:var(--color-accent);background:var(--color-accent-dim)}.footer-author-divider{background:var(--color-mist);width:1px;height:20px;margin:0 var(--spacing-xs)}.footer-newsletter{color:var(--color-text);background:var(--color-paper);border:1px solid var(--color-mist);transition:all var(--duration-fast) var(--ease-out);border-radius:100px;align-items:center;gap:6px;padding:8px 14px;font-size:.875rem;font-weight:500;text-decoration:none;display:inline-flex}.footer-newsletter:hover{border-color:var(--color-accent);color:var(--color-accent)}.footer-newsletter svg{transition:transform var(--duration-fast) var(--ease-out)}.footer-newsletter:hover svg{transform:translate(3px)}@media (max-width:768px){.footer-author{gap:var(--spacing-sm);flex-direction:column}.footer-author-divider{display:none}}.btn{justify-content:center;align-items:center;gap:var(--spacing-xs);font-family:var(--font-body);letter-spacing:.03em;cursor:pointer;transition:all var(--duration-base) var(--ease-out);border:none;padding:1rem 2rem;font-size:.9375rem;font-weight:600;text-decoration:none;display:inline-flex;position:relative;overflow:hidden}.btn-primary{background:var(--color-ink);color:var(--color-paper)}.btn-primary:before{content:"";background:var(--color-accent);transition:transform var(--duration-base) var(--ease-out);z-index:0;position:absolute;inset:0;transform:translateY(100%)}.btn-primary:hover:before{transform:translateY(0)}.btn-primary:hover{color:var(--color-paper)}.btn-primary span,.btn-primary svg,.btn-primary:not(:has(span)){z-index:1;position:relative}.btn-secondary{color:var(--color-ink);border:1px solid var(--color-ink);background:0 0}.btn-secondary:hover{background:var(--color-ink);color:var(--color-paper)}.btn:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}.btn-primary:focus-visible{outline-color:var(--color-paper);box-shadow:0 0 0 4px var(--color-accent)}.btn-secondary:focus-visible{outline-color:var(--color-accent)}@keyframes revealUp{to{opacity:1;transform:translateY(0)}}@keyframes float{0%,to{transform:translate(-50%)translateY(0)}50%{transform:translate(-50%)translateY(-8px)}}@keyframes bounce{0%,to{transform:translateY(0)}50%{transform:translateY(4px)}}[data-reveal]{opacity:0;transition:opacity .8s var(--ease-out), transform .8s var(--ease-out);transform:translateY(30px)}[data-reveal].revealed{opacity:1;transform:translateY(0)}[data-reveal]:first-child{transition-delay:0s}[data-reveal]:nth-child(2){transition-delay:.1s}[data-reveal]:nth-child(3){transition-delay:.2s}[data-reveal]:nth-child(4){transition-delay:.3s}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}html{scroll-behavior:auto}.hero-canvas{display:none}.hero-scroll-indicator{opacity:1;animation:none}[data-reveal],.gallery-frame{opacity:1;transform:none}}.load-error{text-align:center;padding:var(--spacing-2xl) var(--spacing-lg);justify-content:center;align-items:center;gap:var(--spacing-md);background:var(--color-cream);border:1px solid var(--color-mist);border-radius:8px;flex-direction:column;display:flex}.load-error-icon{color:var(--color-accent);font-size:2.5rem}.load-error-title{font-family:var(--font-display);color:var(--color-ink);margin:0;font-size:1.5rem;font-weight:400}.load-error-text{color:var(--color-ash);max-width:40ch;font-size:1rem;line-height:1.5}.load-error-retry{margin-top:var(--spacing-sm)}.bias-tags{align-items:center;gap:var(--spacing-sm);margin-top:var(--spacing-lg);flex-direction:column;display:flex}.bias-tags-label{font-family:var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--color-ash);font-size:.6875rem;font-weight:500}.bias-tags-list{justify-content:center;gap:var(--spacing-xs);flex-wrap:wrap;display:flex}.bias-tag{background:var(--color-cream);border:1px solid var(--color-mist);color:var(--color-charcoal);transition:all var(--duration-fast) var(--ease-out);padding:6px 12px;font-size:.75rem;font-weight:500}.bias-tag:hover{border-color:var(--color-accent);color:var(--color-accent)}.antidote-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist)}.patterns-categories{margin-bottom:var(--spacing-xl)}.pattern-tabs{gap:var(--spacing-xs);margin-bottom:var(--spacing-lg);flex-wrap:wrap;display:flex}.pattern-tab{font-family:var(--font-body);color:var(--color-ash);padding:var(--spacing-xs) var(--spacing-sm);cursor:pointer;transition:color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);background:0 0;border:none;border-bottom:2px solid #0000;font-size:.875rem}.pattern-tab:hover{color:var(--color-charcoal)}.pattern-tab.active{color:var(--color-ink);border-bottom-color:var(--color-accent);font-weight:500}.pattern-tab:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:4px}.pattern-panel{display:none}.pattern-panel.active{display:block}.pattern-columns{gap:var(--spacing-xl);grid-template-columns:1fr 1fr;display:grid}@media (max-width:768px){.pattern-columns{gap:var(--spacing-md);grid-template-columns:1fr}}.pattern-column-label{text-transform:uppercase;letter-spacing:.1em;margin-bottom:var(--spacing-sm);color:var(--color-ash);font-size:.6875rem;font-weight:600;display:block}.pattern-column--anti .pattern-column-label{color:var(--color-accent)}.pattern-column--do .pattern-column-label{color:var(--color-success,#22c55e)}.pattern-list{gap:var(--spacing-xs);flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.pattern-item{padding-left:var(--spacing-md);font-size:.8125rem;line-height:1.5;position:relative}.pattern-item--anti{color:var(--color-ash)}.pattern-item--anti:before{content:"×";color:var(--color-accent);font-weight:600;position:absolute;left:0}.pattern-item--do{color:var(--color-charcoal)}.pattern-item--do:before{content:"✓";color:var(--color-success,#22c55e);font-weight:600;position:absolute;left:0}.contribute-inline{color:var(--color-ash);margin-top:var(--spacing-md);font-size:.875rem}.contribute-inline a{color:var(--color-accent);text-decoration:none}.contribute-inline a:hover{text-decoration:underline}.pillar-item--main{background:var(--color-accent-dim);border:1px solid var(--color-accent)}.pillar-item--main .pillar-item-name{color:var(--color-accent);font-size:1.125rem;font-weight:600}.pillar-item--ref{padding:var(--spacing-xs) var(--spacing-sm);background:0 0}.pillar-item-label{text-transform:uppercase;letter-spacing:.05em;color:var(--color-ash);font-size:.75rem;font-weight:500}.pillar-refs{gap:var(--spacing-xs);padding:0 var(--spacing-sm);flex-wrap:wrap;display:flex}.pillar-ref{text-transform:uppercase;letter-spacing:.03em;background:var(--color-paper);color:var(--color-ash);border:1px solid var(--color-mist);transition:all var(--duration-fast) var(--ease-out);border-radius:3px;padding:4px 10px;font-size:.6875rem;font-weight:500}.pillar-ref:hover{border-color:var(--color-accent);color:var(--color-accent)}.pillar-command-group{align-items:center;gap:var(--spacing-xs);padding:var(--spacing-sm);background:var(--color-paper);border-radius:4px;flex-wrap:wrap;display:flex}.pillar-group-label{text-transform:uppercase;letter-spacing:.05em;color:var(--color-ash);width:100%;margin-bottom:4px;font-size:.6875rem;font-weight:600}.pillar-command-group .pillar-item-code{background:var(--color-accent-dim);border-radius:3px;padding:4px 8px;font-size:.8125rem}.platforms-section{padding:var(--spacing-2xl) 0;border-top:1px solid var(--color-mist)}.platforms-section .section-subtitle{max-width:60ch}.install-terminal{max-width:640px;margin:0 auto var(--spacing-xl)}.install-terminal .glass-terminal{height:auto}.install-terminal .terminal-body{flex-direction:column;padding:0;display:flex}.install-terminal-row{padding:var(--spacing-md) var(--spacing-lg);flex-direction:column;gap:6px;display:flex}.install-terminal-label{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.1em;color:var(--color-ash);font-size:.625rem;font-weight:600}.install-terminal-cmd{align-items:center;gap:var(--spacing-sm);display:flex}.install-terminal-cmd .terminal-prompt{flex-shrink:0}.install-terminal-cmd code{font-family:var(--font-mono);color:var(--color-ink);white-space:nowrap;text-overflow:ellipsis;background:0 0;flex:1;padding:0;font-size:.9375rem;overflow:hidden}.install-terminal-cmd .copy-btn{flex-shrink:0}.install-terminal-cmd .btn{padding:.5rem 1rem;font-size:.8125rem}.install-terminal-note{color:var(--color-ash);padding-left:calc(.75rem + var(--spacing-sm));font-size:.75rem}.install-terminal-note code{font-family:var(--font-mono);background:var(--color-mist);color:var(--color-ink);border-radius:3px;padding:2px 5px;font-size:.6875rem}.install-terminal-divider{background:var(--color-mist);height:1px;margin:0}@media (max-width:600px){.install-terminal-row{padding:var(--spacing-sm) var(--spacing-md)}.install-terminal-cmd code{font-size:.75rem}}.install-providers{align-items:center;gap:var(--spacing-sm);margin-top:var(--spacing-md);flex-direction:column;display:flex}.install-providers-label{text-transform:uppercase;letter-spacing:.1em;color:var(--color-ash);font-size:.75rem;font-weight:600}.install-providers-row{justify-content:center;gap:var(--spacing-sm);flex-wrap:wrap;display:flex}.install-provider-badge{color:var(--color-charcoal);align-items:center;gap:6px;font-size:.8125rem;display:flex}.install-provider-badge img{border-radius:4px}.install-terminal-cmd--download{align-items:center;gap:var(--spacing-sm);flex-wrap:wrap;display:flex}.prefix-toggle{cursor:pointer;transition:border-color var(--duration-fast) var(--ease-out);background:#0000000f;border:1px solid #00000014;border-radius:6px;align-items:center;gap:8px;padding:6px 12px;display:inline-flex}.prefix-toggle:hover{border-color:#0003}.prefix-toggle input{opacity:0;width:0;height:0;position:absolute}.prefix-toggle-slider{background:var(--color-mist);width:32px;height:18px;transition:background var(--duration-fast) var(--ease-out);border-radius:18px;flex-shrink:0;position:relative}.prefix-toggle-slider:after{content:"";background:var(--color-paper);width:14px;height:14px;transition:transform var(--duration-fast) var(--ease-out);border-radius:50%;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #00000026}.prefix-toggle input:checked+.prefix-toggle-slider{background:var(--color-accent)}.prefix-toggle input:checked+.prefix-toggle-slider:after{transform:translate(14px)}.prefix-toggle input:focus-visible+.prefix-toggle-slider{outline:2px solid var(--color-accent);outline-offset:2px}.prefix-toggle-label{color:var(--color-charcoal);white-space:nowrap;font-size:.75rem}.prefix-toggle-label code{font-family:var(--font-mono);background:var(--color-accent-dim);color:var(--color-accent);border-radius:3px;padding:1px 4px;font-size:.6875rem}.has-tooltip{cursor:default;position:relative}.has-tooltip:after{content:attr(data-tooltip);background:var(--color-ink);color:var(--color-paper);white-space:nowrap;pointer-events:none;opacity:0;z-index:100;border-radius:6px;padding:6px 10px;font-size:.6875rem;line-height:1.4;transition:opacity .15s;position:absolute;bottom:calc(100% + 8px);left:50%;transform:translate(-50%)}.has-tooltip:hover:after{opacity:1}.hero-logo-icon{align-items:center;display:inline-flex}.download-tip{color:var(--color-ash);margin-top:var(--spacing-sm);text-align:center;font-size:.8125rem}.download-tip a{color:var(--color-accent);text-decoration:none}.download-tip a:hover{text-decoration:underline}.consulting-section{padding:var(--spacing-xl) 0;border-top:1px solid var(--color-mist)}.consulting-content{justify-content:space-between;align-items:center;gap:var(--spacing-lg);flex-wrap:wrap;display:flex}.consulting-actions{gap:var(--spacing-sm);flex-shrink:0;display:flex}.consulting-text{flex:1;min-width:280px}.consulting-title{margin:0 0 var(--spacing-sm) 0;font-size:clamp(1.5rem,4vw,2rem);font-style:italic;font-weight:300}.consulting-desc{color:var(--color-charcoal);max-width:45ch;margin:0;font-size:1rem;line-height:1.6}@media (max-width:600px){.consulting-content{flex-direction:column;align-items:flex-start}.consulting-actions{flex-direction:column;width:100%}.consulting-actions .btn{justify-content:center;width:100%}} \ No newline at end of file diff --git a/scripts/build.js b/scripts/build.js index 720f9891d..a0499f1ee 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -18,16 +18,7 @@ import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { readSourceFiles, readPatterns } from './lib/utils.js'; -import { - transformCursor, - transformClaudeCode, - transformGemini, - transformCodex, - transformAgents, - transformKiro, - transformOpenCode, - transformPi -} from './lib/transformers/index.js'; +import { createTransformer, PROVIDERS } from './lib/transformers/index.js'; import { createAllZips } from './lib/zip.js'; import { execSync } from 'child_process'; @@ -141,18 +132,9 @@ function assembleUniversal(distDir, suffix = '') { fs.rmSync(universalDir, { recursive: true, force: true }); } - const providerMappings = [ - { provider: 'cursor', configDir: '.cursor' }, - { provider: 'claude-code', configDir: '.claude' }, - { provider: 'gemini', configDir: '.gemini' }, - { provider: 'codex', configDir: '.codex' }, - { provider: 'agents', configDir: '.agents' }, - { provider: 'kiro', configDir: '.kiro' }, - { provider: 'opencode', configDir: '.opencode' }, - { provider: 'pi', configDir: '.pi' } - ]; + const providerConfigs = Object.values(PROVIDERS); - for (const { provider, configDir } of providerMappings) { + for (const { provider, configDir } of providerConfigs) { const src = path.join(distDir, `${provider}${suffix}`, configDir); const dest = path.join(universalDir, configDir); if (fs.existsSync(src)) { @@ -183,7 +165,7 @@ These are hidden folders (dotfiles) — press Cmd+Shift+. in Finder to see them. `); const label = suffix ? ' (prefixed)' : ''; - console.log(`✓ Assembled universal${label} directory (${providerMappings.length} providers)`); + console.log(`✓ Assembled universal${label} directory (${providerConfigs.length} providers)`); } /** @@ -340,26 +322,12 @@ async function build() { const userInvocableCount = skills.filter(s => s.userInvocable).length; console.log(`📖 Read ${skills.length} skills (${userInvocableCount} user-invocable) and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`); - // Transform for each provider - transformCursor(skills, DIST_DIR, patterns); - transformClaudeCode(skills, DIST_DIR, patterns); - transformGemini(skills, DIST_DIR, patterns); - transformCodex(skills, DIST_DIR, patterns); - transformAgents(skills, DIST_DIR, patterns); - transformKiro(skills, DIST_DIR, patterns); - transformOpenCode(skills, DIST_DIR, patterns); - transformPi(skills, DIST_DIR, patterns); - - // Transform for each provider (prefixed with i-) - const prefixOptions = { prefix: 'i-', outputSuffix: '-prefixed' }; - transformCursor(skills, DIST_DIR, patterns, prefixOptions); - transformClaudeCode(skills, DIST_DIR, patterns, prefixOptions); - transformGemini(skills, DIST_DIR, patterns, prefixOptions); - transformCodex(skills, DIST_DIR, patterns, prefixOptions); - transformAgents(skills, DIST_DIR, patterns, prefixOptions); - transformKiro(skills, DIST_DIR, patterns, prefixOptions); - transformOpenCode(skills, DIST_DIR, patterns, prefixOptions); - transformPi(skills, DIST_DIR, patterns, prefixOptions); + // Transform for each provider (unprefixed + prefixed) + for (const config of Object.values(PROVIDERS)) { + const transform = createTransformer(config); + transform(skills, DIST_DIR); + transform(skills, DIST_DIR, { prefix: 'i-', outputSuffix: '-prefixed' }); + } // Assemble universal directory (unprefixed and prefixed) assembleUniversal(DIST_DIR); diff --git a/scripts/lib/transformers/agents.js b/scripts/lib/transformers/agents.js deleted file mode 100644 index 7e27fbf6b..000000000 --- a/scripts/lib/transformers/agents.js +++ /dev/null @@ -1,69 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Agents Transformer (VS Code Copilot + Antigravity) - * - * All skills output to .agents/skills/{name}/SKILL.md - * Frontmatter: name, description, user-invocable (if true), argument-hint (from args) - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused) - * @param {Object} options - Optional settings - */ -export function transformAgents(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const agentsDir = path.join(distDir, `agents${outputSuffix}`); - const skillsDir = path.join(agentsDir, '.agents/skills'); - - cleanDir(agentsDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - - if (skill.userInvocable) frontmatterObj['user-invocable'] = true; - - // Build argument-hint from args array for user-invocable skills - if (skill.userInvocable && skill.args && skill.args.length > 0) { - const hints = skill.args.map(arg => { - return arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=]`; - }); - frontmatterObj['argument-hint'] = hints.join(' '); - } - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - let skillBody = replacePlaceholders(skill.body, 'agents', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'agents'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Agents${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/claude-code.js b/scripts/lib/transformers/claude-code.js deleted file mode 100644 index 365647bda..000000000 --- a/scripts/lib/transformers/claude-code.js +++ /dev/null @@ -1,68 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Claude Code Transformer (Skills Only) - * - * All skills output to .claude/skills/{name}/SKILL.md - * User-invokable skills get args support in frontmatter. - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused, kept for interface consistency) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformClaudeCode(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const claudeDir = path.join(distDir, `claude-code${outputSuffix}`); - const skillsDir = path.join(claudeDir, '.claude/skills'); - - cleanDir(claudeDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - - if (skill.userInvocable) frontmatterObj['user-invocable'] = true; - if (skill.args && skill.args.length > 0) frontmatterObj.args = skill.args; - if (skill.license) frontmatterObj.license = skill.license; - if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility; - if (skill.metadata) frontmatterObj.metadata = skill.metadata; - if (skill.allowedTools) frontmatterObj['allowed-tools'] = skill.allowedTools; - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - let skillBody = replacePlaceholders(skill.body, 'claude-code', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'claude-code'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Claude Code${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/codex.js b/scripts/lib/transformers/codex.js deleted file mode 100644 index 2192e3ee3..000000000 --- a/scripts/lib/transformers/codex.js +++ /dev/null @@ -1,79 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Codex Transformer (Skills Only) - * - * All skills output to .codex/skills/{name}/SKILL.md - * Frontmatter: name, description, argument-hint (from args for user-invocable) - * For user-invocable skills: {{argname}} becomes $ARGNAME in body - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformCodex(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const codexDir = path.join(distDir, `codex${outputSuffix}`); - const skillsDir = path.join(codexDir, '.codex/skills'); - - cleanDir(codexDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - - // Build argument-hint from args array for user-invocable skills - if (skill.userInvocable && skill.args && skill.args.length > 0) { - const hints = skill.args.map(arg => { - return arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=]`; - }); - frontmatterObj['argument-hint'] = hints.join(' '); - } - if (skill.license) frontmatterObj.license = skill.license; - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - - let skillBody = replacePlaceholders(skill.body, 'codex', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - // For user-invocable skills, transform remaining {{argname}} to $ARGNAME - if (skill.userInvocable) { - skillBody = skillBody.replace(/\{\{([^}]+)\}\}/g, (match, argName) => { - return `$${argName.toUpperCase()}`; - }); - } - - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'codex'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Codex${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/cursor.js b/scripts/lib/transformers/cursor.js deleted file mode 100644 index 9d91d7d9f..000000000 --- a/scripts/lib/transformers/cursor.js +++ /dev/null @@ -1,62 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Cursor Transformer (Skills Only) - * - * All skills output to .cursor/skills/{name}/SKILL.md - * Frontmatter: name, description, license - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformCursor(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const cursorDir = path.join(distDir, `cursor${outputSuffix}`); - const skillsDir = path.join(cursorDir, '.cursor/skills'); - - cleanDir(cursorDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - if (skill.license) frontmatterObj.license = skill.license; - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - let skillBody = replacePlaceholders(skill.body, 'cursor', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'cursor'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Cursor${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js new file mode 100644 index 000000000..df7e535f7 --- /dev/null +++ b/scripts/lib/transformers/factory.js @@ -0,0 +1,113 @@ +import path from 'path'; +import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; + +/** + * Map from frontmatter field name to extraction spec. + * + * - sourceKey: property name on the skill object + * - yamlKey: key name in YAML frontmatter + * - condition: if provided, field is only emitted when this returns true + * - value: if provided, use this instead of skill[sourceKey] + */ +const FIELD_SPECS = { + 'user-invocable': { + sourceKey: 'userInvocable', + yamlKey: 'user-invocable', + condition: (skill) => skill.userInvocable, + value: () => true, + }, + 'argument-hint': { + sourceKey: 'argumentHint', + yamlKey: 'argument-hint', + condition: (skill) => skill.userInvocable && skill.argumentHint, + }, + license: { + sourceKey: 'license', + yamlKey: 'license', + }, + compatibility: { + sourceKey: 'compatibility', + yamlKey: 'compatibility', + }, + metadata: { + sourceKey: 'metadata', + yamlKey: 'metadata', + }, + 'allowed-tools': { + sourceKey: 'allowedTools', + yamlKey: 'allowed-tools', + }, +}; + +/** + * Create a transformer function for a given provider config. + * + * @param {Object} config - Provider configuration from providers.js + * @returns {Function} transform(skills, distDir, options?) + */ +export function createTransformer(config) { + const { provider, configDir, displayName, frontmatterFields = [], bodyTransform } = config; + + const activeFields = frontmatterFields + .map((name) => FIELD_SPECS[name]) + .filter(Boolean); + + return function transform(skills, distDir, options = {}) { + const { prefix = '', outputSuffix = '' } = options; + const providerDir = path.join(distDir, `${provider}${outputSuffix}`); + const skillsDir = path.join(providerDir, `${configDir}/skills`); + + cleanDir(providerDir); + ensureDir(skillsDir); + + const allSkillNames = skills.map((s) => s.name); + const commandNames = skills + .filter((s) => s.userInvocable) + .map((s) => `${prefix}${s.name}`); + + let refCount = 0; + + for (const skill of skills) { + const skillName = `${prefix}${skill.name}`; + const skillDir = path.join(skillsDir, skillName); + + // Build frontmatter + const frontmatterObj = { + name: skillName, + description: skill.description, + }; + + for (const spec of activeFields) { + if (spec.condition && !spec.condition(skill)) continue; + const val = spec.value ? spec.value(skill) : skill[spec.sourceKey]; + if (val) frontmatterObj[spec.yamlKey] = val; + } + + const frontmatter = generateYamlFrontmatter(frontmatterObj); + + // Build body + let skillBody = replacePlaceholders(skill.body, provider, commandNames); + if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); + if (bodyTransform) skillBody = bodyTransform(skillBody, skill); + + const content = `${frontmatter}\n\n${skillBody}`; + writeFile(path.join(skillDir, 'SKILL.md'), content); + + // Copy reference files + if (skill.references && skill.references.length > 0) { + const refDir = path.join(skillDir, 'reference'); + ensureDir(refDir); + for (const ref of skill.references) { + const refContent = replacePlaceholders(ref.content, provider); + writeFile(path.join(refDir, `${ref.name}.md`), refContent); + refCount++; + } + } + } + + const userInvocableCount = skills.filter((s) => s.userInvocable).length; + const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; + const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; + console.log(`✓ ${displayName}${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); + }; +} diff --git a/scripts/lib/transformers/gemini.js b/scripts/lib/transformers/gemini.js deleted file mode 100644 index 4b1a6b7f0..000000000 --- a/scripts/lib/transformers/gemini.js +++ /dev/null @@ -1,66 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Gemini Transformer (Skills Only) - * - * All skills output to .gemini/skills/{name}/SKILL.md - * Frontmatter: name, description - * For user-invocable skills: {{arg}} placeholders become {{args}} in body - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformGemini(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const geminiDir = path.join(distDir, `gemini${outputSuffix}`); - const skillsDir = path.join(geminiDir, '.gemini/skills'); - - cleanDir(geminiDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatter = generateYamlFrontmatter({ - name: skillName, - description: skill.description, - }); - - let skillBody = replacePlaceholders(skill.body, 'gemini', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - // For user-invocable skills, replace remaining {{arg}} placeholders with {{args}} - if (skill.userInvocable) { - skillBody = skillBody.replace(/\{\{[^}]+\}\}/g, '{{args}}'); - } - - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'gemini'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Gemini${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/index.js b/scripts/lib/transformers/index.js index da00044f3..c8b8b49ca 100644 --- a/scripts/lib/transformers/index.js +++ b/scripts/lib/transformers/index.js @@ -1,8 +1,13 @@ -export { transformCursor } from './cursor.js'; -export { transformClaudeCode } from './claude-code.js'; -export { transformGemini } from './gemini.js'; -export { transformCodex } from './codex.js'; -export { transformAgents } from './agents.js'; -export { transformKiro } from './kiro.js'; -export { transformOpenCode } from "./opencode.js"; -export { transformPi } from './pi.js'; +import { createTransformer } from './factory.js'; +import { PROVIDERS } from './providers.js'; + +export const transformCursor = createTransformer(PROVIDERS.cursor); +export const transformClaudeCode = createTransformer(PROVIDERS['claude-code']); +export const transformGemini = createTransformer(PROVIDERS.gemini); +export const transformCodex = createTransformer(PROVIDERS.codex); +export const transformAgents = createTransformer(PROVIDERS.agents); +export const transformKiro = createTransformer(PROVIDERS.kiro); +export const transformOpenCode = createTransformer(PROVIDERS.opencode); +export const transformPi = createTransformer(PROVIDERS.pi); + +export { createTransformer, PROVIDERS }; diff --git a/scripts/lib/transformers/kiro.js b/scripts/lib/transformers/kiro.js deleted file mode 100644 index 84a334b1e..000000000 --- a/scripts/lib/transformers/kiro.js +++ /dev/null @@ -1,65 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Kiro Transformer (Skills Only) - * - * All skills output to .kiro/skills/{name}/SKILL.md - * Frontmatter: name, description, license, compatibility, metadata - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformKiro(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const kiroDir = path.join(distDir, `kiro${outputSuffix}`); - const skillsDir = path.join(kiroDir, '.kiro/skills'); - - cleanDir(kiroDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - - if (skill.license) frontmatterObj.license = skill.license; - if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility; - if (skill.metadata) frontmatterObj.metadata = skill.metadata; - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - let skillBody = replacePlaceholders(skill.body, 'kiro', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'kiro'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Kiro${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/opencode.js b/scripts/lib/transformers/opencode.js deleted file mode 100644 index 05181d964..000000000 --- a/scripts/lib/transformers/opencode.js +++ /dev/null @@ -1,84 +0,0 @@ -import path from 'path'; -import { - cleanDir, - ensureDir, - generateYamlFrontmatter, - prefixSkillReferences, - replacePlaceholders, - writeFile, -} from '../utils.js'; - -/** - * OpenCode Transformer (Skills Only) - * - * All skills output to .opencode/skills/{name}/SKILL.md - * User-invokable skills get args support in frontmatter. - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused, kept for interface consistency) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformOpenCode( - skills, - distDir, - patterns = null, - options = {}, -) { - const { prefix = '', outputSuffix = '' } = options; - const opencodeDir = path.join(distDir, `opencode${outputSuffix}`); - const skillsDir = path.join(opencodeDir, '.opencode/skills'); - - cleanDir(opencodeDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map((s) => s.name); - const commandNames = skills - .filter((s) => s.userInvocable) - .map((s) => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - - if (skill.userInvocable) frontmatterObj['user-invocable'] = true; - if (skill.args && skill.args.length > 0) frontmatterObj.args = skill.args; - if (skill.license) frontmatterObj.license = skill.license; - if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility; - if (skill.metadata) frontmatterObj.metadata = skill.metadata; - if (skill.allowedTools) - frontmatterObj['allowed-tools'] = skill.allowedTools; - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - let skillBody = replacePlaceholders(skill.body, 'opencode', commandNames); - if (prefix) - skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'opencode'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter((s) => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ OpenCode${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/pi.js b/scripts/lib/transformers/pi.js deleted file mode 100644 index 88cf97aea..000000000 --- a/scripts/lib/transformers/pi.js +++ /dev/null @@ -1,65 +0,0 @@ -import path from 'path'; -import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js'; - -/** - * Pi Transformer (Skills Only) - * - * All skills output to .pi/skills/{name}/SKILL.md - * Frontmatter: name, description, license, compatibility, metadata - * - * @param {Array} skills - All skills (including user-invocable ones) - * @param {string} distDir - Distribution output directory - * @param {Object} patterns - Design patterns data (unused) - * @param {Object} options - Optional settings - * @param {string} options.prefix - Prefix to add to skill names (e.g., 'i-') - * @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed') - */ -export function transformPi(skills, distDir, patterns = null, options = {}) { - const { prefix = '', outputSuffix = '' } = options; - const piDir = path.join(distDir, `pi${outputSuffix}`); - const skillsDir = path.join(piDir, '.pi/skills'); - - cleanDir(piDir); - ensureDir(skillsDir); - - const allSkillNames = skills.map(s => s.name); - const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`); - let refCount = 0; - for (const skill of skills) { - const skillName = `${prefix}${skill.name}`; - const skillDir = path.join(skillsDir, skillName); - - const frontmatterObj = { - name: skillName, - description: skill.description, - }; - - if (skill.license) frontmatterObj.license = skill.license; - if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility; - if (skill.metadata) frontmatterObj.metadata = skill.metadata; - - const frontmatter = generateYamlFrontmatter(frontmatterObj); - let skillBody = replacePlaceholders(skill.body, 'pi', commandNames); - if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames); - const content = `${frontmatter}\n\n${skillBody}`; - const outputPath = path.join(skillDir, 'SKILL.md'); - writeFile(outputPath, content); - - // Copy reference files if they exist - if (skill.references && skill.references.length > 0) { - const refDir = path.join(skillDir, 'reference'); - ensureDir(refDir); - for (const ref of skill.references) { - const refOutputPath = path.join(refDir, `${ref.name}.md`); - const refContent = replacePlaceholders(ref.content, 'pi'); - writeFile(refOutputPath, refContent); - refCount++; - } - } - } - - const userInvocableCount = skills.filter(s => s.userInvocable).length; - const refInfo = refCount > 0 ? ` (${refCount} reference files)` : ''; - const prefixInfo = prefix ? ` [${prefix}prefixed]` : ''; - console.log(`✓ Pi${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`); -} diff --git a/scripts/lib/transformers/providers.js b/scripts/lib/transformers/providers.js new file mode 100644 index 000000000..d8afc7c07 --- /dev/null +++ b/scripts/lib/transformers/providers.js @@ -0,0 +1,72 @@ +/** + * Provider configurations for the transformer factory. + * + * Each config specifies: + * - provider: key into PROVIDER_PLACEHOLDERS (e.g. 'claude-code') + * - configDir: dot-directory name (e.g. '.claude') + * - displayName: human-readable name for log output (e.g. 'Claude Code') + * - frontmatterFields: which optional fields to emit beyond name + description + * - bodyTransform: optional function (body, skill) => transformed body + */ +export const PROVIDERS = { + cursor: { + provider: 'cursor', + configDir: '.cursor', + displayName: 'Cursor', + frontmatterFields: ['license'], + }, + 'claude-code': { + provider: 'claude-code', + configDir: '.claude', + displayName: 'Claude Code', + frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'], + }, + gemini: { + provider: 'gemini', + configDir: '.gemini', + displayName: 'Gemini', + frontmatterFields: [], + bodyTransform: (body, skill) => { + if (skill.userInvocable) { + return body.replace(/\{\{[^}]+\}\}/g, '{{args}}'); + } + return body; + }, + }, + codex: { + provider: 'codex', + configDir: '.codex', + displayName: 'Codex', + frontmatterFields: ['argument-hint', 'license'], + bodyTransform: (body, skill) => { + if (skill.userInvocable) { + return body.replace(/\{\{([^}]+)\}\}/g, (_, argName) => `$${argName.toUpperCase()}`); + } + return body; + }, + }, + agents: { + provider: 'agents', + configDir: '.agents', + displayName: 'Agents', + frontmatterFields: ['user-invocable', 'argument-hint'], + }, + kiro: { + provider: 'kiro', + configDir: '.kiro', + displayName: 'Kiro', + frontmatterFields: ['license', 'compatibility', 'metadata'], + }, + opencode: { + provider: 'opencode', + configDir: '.opencode', + displayName: 'OpenCode', + frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'], + }, + pi: { + provider: 'pi', + configDir: '.pi', + displayName: 'Pi', + frontmatterFields: ['license', 'compatibility', 'metadata'], + }, +}; diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 50086ece7..86063fd89 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -61,7 +61,11 @@ export function parseFrontmatter(content) { const value = trimmed.slice(colonIndex + 1).trim(); if (value) { - frontmatter[key] = value === 'true' ? true : value === 'false' ? false : value; + // Strip YAML quotes + const unquoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) + ? value.slice(1, -1) + : value; + frontmatter[key] = unquoted === 'true' ? true : unquoted === 'false' ? false : unquoted; currentKey = key; currentArray = null; } else { @@ -148,7 +152,7 @@ export function readSourceFiles(rootDir) { metadata: frontmatter.metadata || null, allowedTools: frontmatter['allowed-tools'] || '', userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true', - args: frontmatter.args || [], + argumentHint: frontmatter['argument-hint'] || '', context: frontmatter.context || null, body, filePath: skillMdPath, @@ -381,7 +385,8 @@ export function generateYamlFrontmatter(data) { } else if (typeof value === 'boolean') { lines.push(`${key}: ${value}`); } else { - lines.push(`${key}: ${value}`); + const needsQuoting = typeof value === 'string' && /^[\[{]/.test(value); + lines.push(`${key}: ${needsQuoting ? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : value}`); } } diff --git a/source/skills/adapt/SKILL.md b/source/skills/adapt/SKILL.md index 0dc9ca859..796eea6d2 100644 --- a/source/skills/adapt/SKILL.md +++ b/source/skills/adapt/SKILL.md @@ -1,13 +1,7 @@ --- name: adapt description: Adapt designs to work across different screen sizes, devices, contexts, or platforms. Ensures consistent experience across varied environments. -args: - - name: target - description: The feature or component to adapt (optional) - required: false - - name: context - description: What to adapt for (mobile, tablet, desktop, print, email, etc.) - required: false +argument-hint: "[target] [context (mobile, tablet, print...)]" user-invocable: true --- diff --git a/source/skills/animate/SKILL.md b/source/skills/animate/SKILL.md index 8789e8e34..458164574 100644 --- a/source/skills/animate/SKILL.md +++ b/source/skills/animate/SKILL.md @@ -1,10 +1,7 @@ --- name: animate description: Review a feature and enhance it with purposeful animations, micro-interactions, and motion effects that improve usability and delight. -args: - - name: target - description: The feature or component to animate (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/arrange/SKILL.md b/source/skills/arrange/SKILL.md index dc3a889cf..aa9b9d6c5 100644 --- a/source/skills/arrange/SKILL.md +++ b/source/skills/arrange/SKILL.md @@ -1,10 +1,7 @@ --- name: arrange description: Improve layout, spacing, and visual rhythm. Fixes monotonous grids, inconsistent spacing, and weak visual hierarchy to create intentional compositions. -args: - - name: target - description: The feature or component to improve layout for (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/audit/SKILL.md b/source/skills/audit/SKILL.md index 7e70200e1..072636456 100644 --- a/source/skills/audit/SKILL.md +++ b/source/skills/audit/SKILL.md @@ -1,10 +1,7 @@ --- name: audit description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations. -args: - - name: area - description: The feature or area to audit (optional) - required: false +argument-hint: "[area (feature, page, component...)]" user-invocable: true --- diff --git a/source/skills/bolder/SKILL.md b/source/skills/bolder/SKILL.md index 387b174a5..a71a88d39 100644 --- a/source/skills/bolder/SKILL.md +++ b/source/skills/bolder/SKILL.md @@ -1,10 +1,7 @@ --- name: bolder description: Amplify safe or boring designs to make them more visually interesting and stimulating. Increases impact while maintaining usability. -args: - - name: target - description: The feature or component to make bolder (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/clarify/SKILL.md b/source/skills/clarify/SKILL.md index 69b75eddb..10a1d1422 100644 --- a/source/skills/clarify/SKILL.md +++ b/source/skills/clarify/SKILL.md @@ -1,10 +1,7 @@ --- name: clarify description: Improve unclear UX copy, error messages, microcopy, labels, and instructions. Makes interfaces easier to understand and use. -args: - - name: target - description: The feature or component with unclear copy (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/colorize/SKILL.md b/source/skills/colorize/SKILL.md index 8ecb974f7..1bc2da5d3 100644 --- a/source/skills/colorize/SKILL.md +++ b/source/skills/colorize/SKILL.md @@ -1,10 +1,7 @@ --- name: colorize description: Add strategic color to features that are too monochromatic or lack visual interest. Makes interfaces more engaging and expressive. -args: - - name: target - description: The feature or component to colorize (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/critique/SKILL.md b/source/skills/critique/SKILL.md index 618e69d55..dcada50ff 100644 --- a/source/skills/critique/SKILL.md +++ b/source/skills/critique/SKILL.md @@ -1,10 +1,7 @@ --- name: critique description: Evaluate design effectiveness from a UX perspective. Assesses visual hierarchy, information architecture, emotional resonance, and overall design quality with actionable feedback. -args: - - name: area - description: The feature or area to critique (optional) - required: false +argument-hint: "[area (feature, page, component...)]" user-invocable: true --- diff --git a/source/skills/delight/SKILL.md b/source/skills/delight/SKILL.md index fae4b827a..e228375ce 100644 --- a/source/skills/delight/SKILL.md +++ b/source/skills/delight/SKILL.md @@ -1,10 +1,7 @@ --- name: delight description: Add moments of joy, personality, and unexpected touches that make interfaces memorable and enjoyable to use. Elevates functional to delightful. -args: - - name: target - description: The feature or area to add delight to (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/distill/SKILL.md b/source/skills/distill/SKILL.md index 866c20c70..8a8b2466d 100644 --- a/source/skills/distill/SKILL.md +++ b/source/skills/distill/SKILL.md @@ -1,10 +1,7 @@ --- name: distill description: Strip designs to their essence by removing unnecessary complexity. Great design is simple, powerful, and clean. -args: - - name: target - description: The feature or component to distill (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/extract/SKILL.md b/source/skills/extract/SKILL.md index 252a31e09..25e73fee8 100644 --- a/source/skills/extract/SKILL.md +++ b/source/skills/extract/SKILL.md @@ -1,10 +1,7 @@ --- name: extract description: Extract and consolidate reusable components, design tokens, and patterns into your design system. Identifies opportunities for systematic reuse and enriches your component library. -args: - - name: target - description: The feature, component, or area to extract from (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/harden/SKILL.md b/source/skills/harden/SKILL.md index eda956685..814db3f64 100644 --- a/source/skills/harden/SKILL.md +++ b/source/skills/harden/SKILL.md @@ -1,10 +1,7 @@ --- name: harden description: Improve interface resilience through better error handling, i18n support, text overflow handling, and edge case management. Makes interfaces robust and production-ready. -args: - - name: target - description: The feature or area to harden (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/normalize/SKILL.md b/source/skills/normalize/SKILL.md index cae143ee4..9f5b0af9f 100644 --- a/source/skills/normalize/SKILL.md +++ b/source/skills/normalize/SKILL.md @@ -1,10 +1,7 @@ --- name: normalize description: Normalize design to match your design system and ensure consistency -args: - - name: feature - description: The page, route, or feature to normalize (optional) - required: false +argument-hint: "[feature (page, route, component...)]" user-invocable: true --- diff --git a/source/skills/onboard/SKILL.md b/source/skills/onboard/SKILL.md index 1944d0b1f..6a9e7260c 100644 --- a/source/skills/onboard/SKILL.md +++ b/source/skills/onboard/SKILL.md @@ -1,10 +1,7 @@ --- name: onboard description: Design or improve onboarding flows, empty states, and first-time user experiences. Helps users get started successfully and understand value quickly. -args: - - name: target - description: The feature or area needing onboarding (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/optimize/SKILL.md b/source/skills/optimize/SKILL.md index 724a1e8c0..de2994bdc 100644 --- a/source/skills/optimize/SKILL.md +++ b/source/skills/optimize/SKILL.md @@ -1,10 +1,7 @@ --- name: optimize description: Improve interface performance across loading speed, rendering, animations, images, and bundle size. Makes experiences faster and smoother. -args: - - name: target - description: The feature or area to optimize (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/overdrive/SKILL.md b/source/skills/overdrive/SKILL.md index 199632a8b..ba64be3bb 100644 --- a/source/skills/overdrive/SKILL.md +++ b/source/skills/overdrive/SKILL.md @@ -1,10 +1,7 @@ --- name: overdrive description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?" -args: - - name: target - description: The feature or area to push into overdrive (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/polish/SKILL.md b/source/skills/polish/SKILL.md index ae348bfa5..927ac3d63 100644 --- a/source/skills/polish/SKILL.md +++ b/source/skills/polish/SKILL.md @@ -1,10 +1,7 @@ --- name: polish description: Final quality pass before shipping. Fixes alignment, spacing, consistency, and detail issues that separate good from great. -args: - - name: target - description: The feature or area to polish (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/quieter/SKILL.md b/source/skills/quieter/SKILL.md index 190d35241..c05009f10 100644 --- a/source/skills/quieter/SKILL.md +++ b/source/skills/quieter/SKILL.md @@ -1,10 +1,7 @@ --- name: quieter description: Tone down overly bold or visually aggressive designs. Reduces intensity while maintaining design quality and impact. -args: - - name: target - description: The feature or component to make quieter (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/source/skills/typeset/SKILL.md b/source/skills/typeset/SKILL.md index e5c792e54..755bba324 100644 --- a/source/skills/typeset/SKILL.md +++ b/source/skills/typeset/SKILL.md @@ -1,10 +1,7 @@ --- name: typeset description: Improve typography by fixing font choices, hierarchy, sizing, weight consistency, and readability. Makes text feel intentional and polished. -args: - - name: target - description: The feature or component to improve typography for (optional) - required: false +argument-hint: "[target]" user-invocable: true --- diff --git a/tests/build.test.js b/tests/build.test.js index cdcd8bac3..78a831f83 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -167,10 +167,7 @@ This is a test skill body.`; name: audit description: Run technical quality checks user-invocable: true -args: - - name: target - description: Target element - required: false +argument-hint: "[TARGET=]" --- Please audit {{target}} for technical quality. Ask {{model}} for help.`; @@ -195,7 +192,7 @@ Please audit {{target}} for technical quality. Ask {{model}} for help.`; expect(cursorContent).toContain('{{target}}'); expect(cursorContent).toContain('the model'); - // Verify Claude Code: full frontmatter with user-invocable and args + // Verify Claude Code: full frontmatter with user-invocable and argument-hint const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8'); expect(claudeContent).toContain('---'); expect(claudeContent).toContain('name: audit'); diff --git a/tests/lib/transformers/agents.test.js b/tests/lib/transformers/agents.test.js deleted file mode 100644 index 09667de48..000000000 --- a/tests/lib/transformers/agents.test.js +++ /dev/null @@ -1,330 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformAgents } from '../../../scripts/lib/transformers/agents.js'; -import { parseFrontmatter } from '../../../scripts/lib/utils.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-agents'); - -describe('transformAgents', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - transformAgents([], TEST_DIR); - expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills'))).toBe(true); - }); - - test('should create skill with full frontmatter', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'agents/.agents/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.name).toBe('test-skill'); - expect(parsed.frontmatter.description).toBe('A test skill'); - expect(parsed.body).toBe('Skill instructions.'); - }); - - test('should add user-invocable flag for user-invocable skills', () => { - const skills = [ - { - name: 'audit', - description: 'Audit command', - userInvocable: true, - body: 'Audit the code.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/audit/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['user-invocable']).toBe(true); - }); - - test('should not add user-invocable flag for non-user-invocable skills', () => { - const skills = [ - { - name: 'helper', - description: 'Helper skill', - body: 'Helper body.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/helper/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['user-invocable']).toBeUndefined(); - }); - - test('should create argument-hint for required args', () => { - const skills = [ - { - name: 'with-args', - description: 'Command with args', - userInvocable: true, - args: [ - { name: 'target', description: 'Target', required: true }, - { name: 'format', description: 'Format', required: false } - ], - body: 'Process {{target}} in {{format}}.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/with-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBe(' [FORMAT=]'); - }); - - test('should not add argument-hint for skills without args', () => { - const skills = [ - { - name: 'no-args', - description: 'No args', - userInvocable: true, - args: [], - body: 'Simple body.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/no-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBeUndefined(); - }); - - test('should not add argument-hint for non-user-invocable skills with args', () => { - const skills = [ - { - name: 'internal', - description: 'Internal skill', - userInvocable: false, - args: [{ name: 'target', description: 'Target', required: true }], - body: 'Body.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/internal/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBeUndefined(); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, - { name: 'skill3', description: 'Skill 3', body: 'Body 3' } - ]; - - transformAgents(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/skill2/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/skill3/SKILL.md'))).toBe(true); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Ask {{model}} for help.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask the model for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'See {{config_file}} for details.' - } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See .github/copilot-instructions.md for details.'); - }); - - test('should replace {{available_commands}} placeholder', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformAgents(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/audit/SKILL.md'), 'utf-8'); - expect(content).toContain('Available: /audit, /polish'); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, - { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } - ] - } - ]; - - transformAgents(skills, TEST_DIR); - - const typoPath = path.join(TEST_DIR, 'agents/.agents/skills/frontend-design/reference/typography.md'); - const colorPath = path.join(TEST_DIR, 'agents/.agents/skills/frontend-design/reference/color.md'); - - expect(fs.existsSync(typoPath)).toBe(true); - expect(fs.existsSync(colorPath)).toBe(true); - expect(fs.readFileSync(typoPath, 'utf-8')).toBe('Typography reference'); - }); - - test('should replace placeholders in reference files', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Body.', - references: [ - { name: 'ref', content: 'Use {{model}} with {{config_file}}.', filePath: '/fake/ref.md' } - ] - } - ]; - - transformAgents(skills, TEST_DIR); - - const refContent = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/test/reference/ref.md'), 'utf-8'); - expect(refContent).toContain('Use the model with .github/copilot-instructions.md.'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' } - ]; - - transformAgents(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const outputPath = path.join(TEST_DIR, 'agents-prefixed/.agents/skills/i-audit/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should prefix skill references in body when prefix is set', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformAgents(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const content = fs.readFileSync(path.join(TEST_DIR, 'agents-prefixed/.agents/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('/i-polish'); - expect(content).toContain('the i-audit skill'); - }); - - test('should clean existing directory before writing', () => { - const existingDir = path.join(TEST_DIR, 'agents/.agents/skills/old'); - fs.mkdirSync(existingDir, { recursive: true }); - fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); - - const skills = [{ name: 'new', description: 'New', body: 'New' }]; - transformAgents(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/old/SKILL.md'))).toBe(false); - expect(fs.existsSync(path.join(TEST_DIR, 'agents/.agents/skills/new/SKILL.md'))).toBe(true); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', userInvocable: false, body: 'body' } - ]; - - transformAgents(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Agents:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should log reference file count', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Body.', - references: [ - { name: 'ref1', content: 'Ref 1', filePath: '/fake/ref1.md' }, - { name: 'ref2', content: 'Ref 2', filePath: '/fake/ref2.md' } - ] - } - ]; - - transformAgents(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 reference files')); - }); - - test('should handle empty skills array', () => { - transformAgents([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'agents/.agents/skills')); - expect(skillDirs).toHaveLength(0); - }); -}); diff --git a/tests/lib/transformers/claude-code.test.js b/tests/lib/transformers/claude-code.test.js deleted file mode 100644 index fdaf35909..000000000 --- a/tests/lib/transformers/claude-code.test.js +++ /dev/null @@ -1,356 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformClaudeCode } from '../../../scripts/lib/transformers/claude-code.js'; -import { parseFrontmatter } from '../../../scripts/lib/utils.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-claude'); - -describe('transformClaudeCode', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - const skills = []; - - transformClaudeCode(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills'))).toBe(true); - }); - - test('should create skill with full frontmatter', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'claude-code/.claude/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.name).toBe('test-skill'); - expect(parsed.frontmatter.description).toBe('A test skill'); - expect(parsed.frontmatter.license).toBe('MIT'); - expect(parsed.body).toBe('Skill instructions.'); - }); - - test('should add user-invocable flag for user-invocable skills', () => { - const skills = [ - { - name: 'audit', - description: 'Audit command', - userInvocable: true, - body: 'Audit the code.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['user-invocable']).toBe(true); - }); - - test('should include args in frontmatter for user-invocable skills', () => { - const skills = [ - { - name: 'test-command', - description: 'A test command', - userInvocable: true, - args: [ - { name: 'target', description: 'The target', required: false }, - { name: 'output', description: 'Output format', required: true } - ], - body: 'Command body here.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'claude-code/.claude/skills/test-command/SKILL.md'); - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.args).toBeArray(); - expect(parsed.frontmatter.args).toHaveLength(2); - expect(parsed.frontmatter.args[0].name).toBe('target'); - expect(parsed.frontmatter.args[1].required).toBe(true); - }); - - test('should handle skills without args', () => { - const skills = [ - { - name: 'simple-skill', - description: 'Simple skill', - userInvocable: true, - args: [], - body: 'Simple body.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/simple-skill/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.args).toBeUndefined(); - }); - - test('should handle skills without license', () => { - const skills = [ - { - name: 'no-license-skill', - description: 'Skill without license', - license: '', - body: 'Body content.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/no-license-skill/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.license).toBeUndefined(); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' } - ]; - - transformClaudeCode(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/skill2/SKILL.md'))).toBe(true); - }); - - test('should clean existing directory before writing', () => { - // Create a pre-existing file structure - const existingDir = path.join(TEST_DIR, 'claude-code/.claude/skills/old'); - fs.mkdirSync(existingDir, { recursive: true }); - fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); - - const skills = [{ name: 'new', description: 'New', license: '', body: 'New' }]; - transformClaudeCode(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/old/SKILL.md'))).toBe(false); - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/new/SKILL.md'))).toBe(true); - }); - - test('should preserve {{placeholder}} syntax in body', () => { - const skills = [ - { - name: 'with-placeholder', - description: 'Has placeholder', - userInvocable: true, - args: [{ name: 'target', description: 'Target', required: false }], - body: 'Process {{target}} and generate output.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/with-placeholder/SKILL.md'), 'utf-8'); - expect(content).toContain('{{target}}'); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - license: 'MIT', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, - { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } - ] - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/frontend-design/reference/typography.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code/.claude/skills/frontend-design/reference/color.md'))).toBe(true); - - const typoContent = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/frontend-design/reference/typography.md'), 'utf-8'); - expect(typoContent).toBe('Typography reference'); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', license: '', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', license: '', userInvocable: false, body: 'body' } - ]; - - transformClaudeCode(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Claude Code:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should handle empty arrays', () => { - transformClaudeCode([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'claude-code/.claude/skills')); - expect(skillDirs).toHaveLength(0); - }); - - test('should format frontmatter correctly with args', () => { - const skills = [ - { - name: 'test', - description: 'Test command', - userInvocable: true, - args: [ - { name: 'arg1', description: 'First arg', required: true }, - { name: 'arg2', description: 'Second arg', required: false } - ], - body: 'Body' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8'); - - expect(content).toContain('---'); - expect(content).toContain('name: test'); - expect(content).toContain('description: Test command'); - expect(content).toContain('args:'); - expect(content).toContain('- name: arg1'); - expect(content).toContain('description: First arg'); - expect(content).toContain('required: true'); - expect(content).toContain('- name: arg2'); - expect(content).toContain('required: false'); - }); - - test('should preserve multiline body content', () => { - const skills = [ - { - name: 'multiline', - description: 'Test', - license: '', - body: `First paragraph. - -Second paragraph with details. - -- List item 1 -- List item 2` - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/multiline/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.body).toContain('First paragraph.'); - expect(parsed.body).toContain('Second paragraph'); - expect(parsed.body).toContain('- List item 1'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', license: '', userInvocable: true, body: 'Audit body' } - ]; - - transformClaudeCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - expect(fs.existsSync(path.join(TEST_DIR, 'claude-code-prefixed/.claude/skills/i-audit/SKILL.md'))).toBe(true); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code-prefixed/.claude/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should include compatibility in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - compatibility: 'claude-code', - body: 'Body' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('compatibility: claude-code'); - }); - - test('should include allowed-tools in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - allowedTools: 'Bash,Edit', - body: 'Body' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('allowed-tools: Bash,Edit'); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Ask {{model}} for help.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask Claude for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'See {{config_file}} for more.' - } - ]; - - transformClaudeCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See CLAUDE.md for more.'); - }); -}); diff --git a/tests/lib/transformers/codex.test.js b/tests/lib/transformers/codex.test.js deleted file mode 100644 index 70e95850f..000000000 --- a/tests/lib/transformers/codex.test.js +++ /dev/null @@ -1,308 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformCodex } from '../../../scripts/lib/transformers/codex.js'; -import { parseFrontmatter } from '../../../scripts/lib/utils.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-codex'); - -describe('transformCodex', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - const skills = []; - - transformCodex(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills'))).toBe(true); - }); - - test('should create skill files with frontmatter and body in .codex/skills/ directory', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions here.' - } - ]; - - transformCodex(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'codex/.codex/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.name).toBe('test-skill'); - expect(parsed.frontmatter.description).toBe('A test skill'); - expect(parsed.body).toBe('Skill instructions here.'); - }); - - test('should create argument-hint for required args', () => { - const skills = [ - { - name: 'with-args', - description: 'Command with args', - userInvocable: true, - args: [ - { name: 'target', description: 'Target', required: true }, - { name: 'output', description: 'Output', required: true } - ], - body: 'Body' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/with-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBe(' '); - }); - - test('should create argument-hint for optional args', () => { - const skills = [ - { - name: 'optional-args', - description: 'Command with optional args', - userInvocable: true, - args: [ - { name: 'format', description: 'Format', required: false } - ], - body: 'Body' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/optional-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBe('[FORMAT=]'); - }); - - test('should create argument-hint with mixed required and optional args', () => { - const skills = [ - { - name: 'mixed-args', - description: 'Mixed args', - userInvocable: true, - args: [ - { name: 'input', description: 'Input', required: true }, - { name: 'format', description: 'Format', required: false }, - { name: 'output', description: 'Output', required: true } - ], - body: 'Body' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/mixed-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBe(' [FORMAT=] '); - }); - - test('should transform {{argname}} to $ARGNAME for user-invocable skills', () => { - const skills = [ - { - name: 'normalize', - description: 'Normalize', - userInvocable: true, - args: [{ name: 'target', description: 'Target', required: false }], - body: 'Please normalize {{target}} to match the design system.' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/normalize/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.body).toContain('$TARGET'); - expect(parsed.body).not.toContain('{{target}}'); - }); - - test('should transform multiple different placeholders', () => { - const skills = [ - { - name: 'multi-arg', - description: 'Multiple args', - userInvocable: true, - args: [], - body: 'Process {{input}} and output to {{output}} with {{format}}.' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/multi-arg/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.body).toContain('$INPUT'); - expect(parsed.body).toContain('$OUTPUT'); - expect(parsed.body).toContain('$FORMAT'); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' }, - { name: 'skill3', description: 'Skill 3', license: 'MIT', body: 'Body 3' } - ]; - - transformCodex(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/skill2/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/skill3/SKILL.md'))).toBe(true); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - license: 'MIT', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' } - ] - } - ]; - - transformCodex(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'codex/.codex/skills/frontend-design/reference/typography.md'))).toBe(true); - - const typoContent = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/frontend-design/reference/typography.md'), 'utf-8'); - expect(typoContent).toBe('Typography reference'); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', license: '', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', license: '', userInvocable: false, body: 'body' } - ]; - - transformCodex(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Codex:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should handle empty arrays', () => { - transformCodex([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'codex/.codex/skills')); - expect(skillDirs).toHaveLength(0); - }); - - test('should handle user-invocable skills without args', () => { - const skills = [ - { - name: 'no-args', - description: 'No args command', - userInvocable: true, - args: [], - body: 'Body content' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/no-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['argument-hint']).toBeUndefined(); - }); - - test('should preserve multiline body', () => { - const skills = [ - { - name: 'multiline', - description: 'Test', - license: '', - body: `First line. - -Second line after blank. - -- Bullet 1 -- Bullet 2` - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/multiline/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.body).toContain('First line.\n\nSecond line'); - expect(parsed.body).toContain('- Bullet 1\n- Bullet 2'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', license: '', userInvocable: true, body: 'Audit body' } - ]; - - transformCodex(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - expect(fs.existsSync(path.join(TEST_DIR, 'codex-prefixed/.codex/skills/i-audit/SKILL.md'))).toBe(true); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex-prefixed/.codex/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Ask {{model}} for help.' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask GPT for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'See {{config_file}} for more.' - } - ]; - - transformCodex(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'codex/.codex/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See AGENTS.md for more.'); - }); -}); diff --git a/tests/lib/transformers/cursor.test.js b/tests/lib/transformers/cursor.test.js deleted file mode 100644 index 23ba63df2..000000000 --- a/tests/lib/transformers/cursor.test.js +++ /dev/null @@ -1,245 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformCursor } from '../../../scripts/lib/transformers/cursor.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-cursor'); - -describe('transformCursor', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - const skills = []; - - transformCursor(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills'))).toBe(true); - }); - - test('should create skill with frontmatter and body', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions here.' - } - ]; - - transformCursor(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'cursor/.cursor/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('---'); - expect(content).toContain('name: test-skill'); - expect(content).toContain('description: A test skill'); - expect(content).toContain('license: MIT'); - expect(content).toContain('Skill instructions here.'); - }); - - test('should handle skills without license', () => { - const skills = [ - { - name: 'no-license-skill', - description: 'Skill without license', - license: '', - body: 'Body content.' - } - ]; - - transformCursor(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/no-license-skill/SKILL.md'), 'utf-8'); - expect(content).not.toContain('license:'); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Skill body 1' }, - { name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Skill body 2' } - ]; - - transformCursor(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/skill2/SKILL.md'))).toBe(true); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - license: 'MIT', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, - { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } - ] - } - ]; - - transformCursor(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/frontend-design/reference/typography.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/frontend-design/reference/color.md'))).toBe(true); - - const typoContent = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/frontend-design/reference/typography.md'), 'utf-8'); - expect(typoContent).toBe('Typography reference'); - }); - - test('should handle skills without references', () => { - const skills = [ - { - name: 'simple-skill', - description: 'Simple', - license: '', - body: 'Body', - references: [] - } - ]; - - transformCursor(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/simple-skill/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/simple-skill/reference'))).toBe(false); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: '', license: '', userInvocable: true, body: 'body1' }, - { name: 'skill2', description: '', license: '', userInvocable: false, body: 'body2' } - ]; - - transformCursor(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Cursor:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should handle empty skills array', () => { - transformCursor([], TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills'))).toBe(true); - }); - - test('should preserve line breaks and formatting in body', () => { - const skills = [ - { - name: 'formatted', - description: 'Test', - license: '', - body: `Line 1 - -Line 3 after blank line - -- Bullet 1 -- Bullet 2 - -End.` - } - ]; - - transformCursor(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/formatted/SKILL.md'), 'utf-8'); - expect(content).toContain('Line 1\n\nLine 3'); - expect(content).toContain('- Bullet 1\n- Bullet 2'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', license: '', body: 'Audit body' } - ]; - - transformCursor(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor-prefixed/.cursor/skills/i-audit/SKILL.md'))).toBe(true); - - const content = fs.readFileSync(path.join(TEST_DIR, 'cursor-prefixed/.cursor/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Ask {{model}} for help.' - } - ]; - - transformCursor(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask the model for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'See {{config_file}} for more.' - } - ]; - - transformCursor(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See .cursorrules for more.'); - }); - - test('should replace {{ask_instruction}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'When unsure, {{ask_instruction}}' - } - ]; - - transformCursor(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.cursor/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('When unsure, ask the user directly to clarify what you cannot infer.'); - }); - - test('should clean existing directory before writing', () => { - // Create a pre-existing file structure - const existingDir = path.join(TEST_DIR, 'cursor/.cursor/skills/old-skill'); - fs.mkdirSync(existingDir, { recursive: true }); - fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old content'); - - const skills = [ - { name: 'new-skill', description: 'New', license: '', body: 'New body' } - ]; - - transformCursor(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/old-skill/SKILL.md'))).toBe(false); - expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.cursor/skills/new-skill/SKILL.md'))).toBe(true); - }); -}); diff --git a/tests/lib/transformers/factory.test.js b/tests/lib/transformers/factory.test.js new file mode 100644 index 000000000..d6e0f5269 --- /dev/null +++ b/tests/lib/transformers/factory.test.js @@ -0,0 +1,276 @@ +import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; +import fs from 'fs'; +import path from 'path'; +import { createTransformer } from '../../../scripts/lib/transformers/factory.js'; +import { parseFrontmatter } from '../../../scripts/lib/utils.js'; + +const TEST_DIR = path.join(process.cwd(), 'test-tmp-factory'); + +// Minimal config using 'cursor' as provider (has existing PROVIDER_PLACEHOLDERS) +const baseConfig = { + provider: 'cursor', + configDir: '.test', + displayName: 'Test Provider', + frontmatterFields: [], +}; + +describe('createTransformer factory', () => { + beforeEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + afterEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + test('should create correct directory structure', () => { + const transform = createTransformer(baseConfig); + transform([], TEST_DIR); + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills'))).toBe(true); + }); + + test('should always emit name and description', () => { + const transform = createTransformer(baseConfig); + const skills = [{ name: 'test', description: 'A test skill', body: 'Body.' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + expect(parsed.frontmatter.name).toBe('test'); + expect(parsed.frontmatter.description).toBe('A test skill'); + expect(parsed.body).toBe('Body.'); + }); + + test('should only emit allowlisted fields', () => { + const config = { ...baseConfig, frontmatterFields: ['license'] }; + const transform = createTransformer(config); + const skills = [{ + name: 'test', + description: 'Test', + license: 'MIT', + compatibility: 'all', + metadata: 'meta', + body: 'Body' + }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + expect(parsed.frontmatter.license).toBe('MIT'); + expect(parsed.frontmatter.compatibility).toBeUndefined(); + expect(parsed.frontmatter.metadata).toBeUndefined(); + }); + + test('should skip empty optional fields', () => { + const config = { ...baseConfig, frontmatterFields: ['license'] }; + const transform = createTransformer(config); + const skills = [{ name: 'test', description: 'Test', license: '', body: 'Body' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + expect(parsed.frontmatter.license).toBeUndefined(); + }); + + test('should emit user-invocable as true when skill is user-invocable', () => { + const config = { ...baseConfig, frontmatterFields: ['user-invocable'] }; + const transform = createTransformer(config); + const skills = [{ name: 'test', description: 'Test', userInvocable: true, body: 'Body' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + expect(parsed.frontmatter['user-invocable']).toBe(true); + }); + + test('should not emit user-invocable when skill is not user-invocable', () => { + const config = { ...baseConfig, frontmatterFields: ['user-invocable'] }; + const transform = createTransformer(config); + const skills = [{ name: 'test', description: 'Test', userInvocable: false, body: 'Body' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + expect(parsed.frontmatter['user-invocable']).toBeUndefined(); + }); + + test('should emit argument-hint only when user-invocable', () => { + const config = { ...baseConfig, frontmatterFields: ['argument-hint'] }; + const transform = createTransformer(config); + + // User-invocable with hint + const skills1 = [{ name: 'test', description: 'Test', userInvocable: true, argumentHint: '[target]', body: 'Body' }]; + transform(skills1, TEST_DIR); + let content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + let parsed = parseFrontmatter(content); + expect(parsed.frontmatter['argument-hint']).toBe('[target]'); + + // Non-user-invocable with hint + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + const skills2 = [{ name: 'test', description: 'Test', userInvocable: false, argumentHint: '[target]', body: 'Body' }]; + transform(skills2, TEST_DIR); + content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + parsed = parseFrontmatter(content); + expect(parsed.frontmatter['argument-hint']).toBeUndefined(); + }); + + test('should apply bodyTransform after placeholder replacement', () => { + const config = { + ...baseConfig, + bodyTransform: (body) => body.replace(/PLACEHOLDER/, 'TRANSFORMED'), + }; + const transform = createTransformer(config); + const skills = [{ name: 'test', description: 'Test', body: 'PLACEHOLDER content' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('TRANSFORMED content'); + }); + + test('should support prefix option', () => { + const transform = createTransformer(baseConfig); + const skills = [{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Body' }]; + transform(skills, TEST_DIR, { prefix: 'i-', outputSuffix: '-prefixed' }); + + const outputPath = path.join(TEST_DIR, 'cursor-prefixed/.test/skills/i-audit/SKILL.md'); + expect(fs.existsSync(outputPath)).toBe(true); + const content = fs.readFileSync(outputPath, 'utf-8'); + expect(content).toContain('name: i-audit'); + }); + + test('should copy reference files', () => { + const transform = createTransformer(baseConfig); + const skills = [{ + name: 'test', + description: 'Test', + body: 'Body', + references: [ + { name: 'ref1', content: 'Reference 1 content', filePath: '/fake/ref1.md' }, + { name: 'ref2', content: 'Reference 2 content', filePath: '/fake/ref2.md' }, + ] + }]; + transform(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/test/reference/ref1.md'))).toBe(true); + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/test/reference/ref2.md'))).toBe(true); + const ref1 = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/reference/ref1.md'), 'utf-8'); + expect(ref1).toBe('Reference 1 content'); + }); + + test('should clean existing directory before writing', () => { + const transform = createTransformer(baseConfig); + const existingDir = path.join(TEST_DIR, 'cursor/.test/skills/old'); + fs.mkdirSync(existingDir, { recursive: true }); + fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); + + const skills = [{ name: 'new', description: 'New', body: 'New' }]; + transform(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/old/SKILL.md'))).toBe(false); + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/new/SKILL.md'))).toBe(true); + }); + + test('should log correct summary', () => { + const consoleMock = mock(() => {}); + const originalLog = console.log; + console.log = consoleMock; + + const transform = createTransformer(baseConfig); + const skills = [ + { name: 's1', description: 'Test', userInvocable: true, body: 'body' }, + { name: 's2', description: 'Test', userInvocable: false, body: 'body' } + ]; + transform(skills, TEST_DIR); + + console.log = originalLog; + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Test Provider:')); + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); + expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); + }); + + test('should handle empty skills array', () => { + const transform = createTransformer(baseConfig); + transform([], TEST_DIR); + + const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'cursor/.test/skills')); + expect(skillDirs).toHaveLength(0); + }); + + test('should replace {{model}} placeholder', () => { + const transform = createTransformer(baseConfig); + const skills = [{ name: 'test', description: 'Test', body: 'Ask {{model}} for help.' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('Ask the model for help.'); + }); + + test('should replace {{config_file}} placeholder', () => { + const transform = createTransformer(baseConfig); + const skills = [{ name: 'test', description: 'Test', body: 'See {{config_file}}.' }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('See .cursorrules.'); + }); + + test('should handle multiple skills', () => { + const transform = createTransformer(baseConfig); + const skills = [ + { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, + { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, + ]; + transform(skills, TEST_DIR); + + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/skill1/SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(TEST_DIR, 'cursor/.test/skills/skill2/SKILL.md'))).toBe(true); + }); + + test('should preserve multiline body content', () => { + const transform = createTransformer(baseConfig); + const skills = [{ + name: 'test', + description: 'Test', + body: `First paragraph.\n\nSecond paragraph.\n\n- List item 1\n- List item 2` + }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + const parsed = parseFrontmatter(content); + expect(parsed.body).toContain('First paragraph.'); + expect(parsed.body).toContain('Second paragraph.'); + expect(parsed.body).toContain('- List item 1'); + }); + + test('should emit all spec fields when configured', () => { + const config = { + ...baseConfig, + frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'], + }; + const transform = createTransformer(config); + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: true, + argumentHint: '[target]', + license: 'MIT', + compatibility: 'claude-code', + metadata: 'v1', + allowedTools: 'Bash,Edit', + body: 'Body' + }]; + transform(skills, TEST_DIR); + + const content = fs.readFileSync(path.join(TEST_DIR, 'cursor/.test/skills/test/SKILL.md'), 'utf-8'); + expect(content).toContain('user-invocable: true'); + expect(content).toContain('argument-hint:'); + expect(content).toContain('license: MIT'); + expect(content).toContain('compatibility: claude-code'); + expect(content).toContain('metadata: v1'); + expect(content).toContain('allowed-tools: Bash,Edit'); + }); +}); diff --git a/tests/lib/transformers/gemini.test.js b/tests/lib/transformers/gemini.test.js deleted file mode 100644 index 9908bed11..000000000 --- a/tests/lib/transformers/gemini.test.js +++ /dev/null @@ -1,210 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformGemini } from '../../../scripts/lib/transformers/gemini.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-gemini'); - -describe('transformGemini', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - const skills = []; - - transformGemini(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills'))).toBe(true); - }); - - test('should create skill files with frontmatter and body in .gemini/skills/ directory', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions here.' - } - ]; - - transformGemini(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'gemini/.gemini/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('---'); - expect(content).toContain('name: test-skill'); - expect(content).toContain('description: A test skill'); - expect(content).toContain('Skill instructions here.'); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', license: 'MIT', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', license: 'Apache', body: 'Body 2' }, - { name: 'skill3', description: 'Skill 3', license: 'MIT', body: 'Body 3' } - ]; - - transformGemini(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill2/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill3/SKILL.md'))).toBe(true); - }); - - test('should handle user-invocable skills with args', () => { - const skills = [ - { - name: 'normalize', - description: 'Normalize design', - userInvocable: true, - args: [{ name: 'target', description: 'Target', required: false }], - body: 'Please normalize {{target}} to match the design system.' - } - ]; - - transformGemini(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/normalize/SKILL.md'), 'utf-8'); - // For user-invocable skills, {{arg}} placeholders become {{args}} - expect(content).toContain('{{args}}'); - expect(content).not.toContain('{{target}}'); - }); - - test('should replace multiple different placeholders with {{args}} for user-invocable skills', () => { - const skills = [ - { - name: 'multi-arg', - description: 'Multiple args', - userInvocable: true, - args: [], - body: 'Process {{input}} and output to {{output}} with {{format}}.' - } - ]; - - transformGemini(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/multi-arg/SKILL.md'), 'utf-8'); - const argsMatches = content.match(/\{\{args\}\}/g); - expect(argsMatches).toHaveLength(3); - }); - - test('should not replace placeholders for non-user-invocable skills', () => { - const skills = [ - { - name: 'passive-skill', - description: 'Passive skill', - userInvocable: false, - body: 'Process {{target}} normally.' - } - ]; - - transformGemini(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/passive-skill/SKILL.md'), 'utf-8'); - expect(content).toContain('{{target}}'); - expect(content).not.toContain('{{args}}'); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - license: 'MIT', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' } - ] - } - ]; - - transformGemini(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/frontend-design/reference/typography.md'))).toBe(true); - - const typoContent = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/frontend-design/reference/typography.md'), 'utf-8'); - expect(typoContent).toBe('Typography reference'); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', license: '', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', license: '', userInvocable: false, body: 'body' } - ]; - - transformGemini(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Gemini:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should handle empty arrays', () => { - transformGemini([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'gemini/.gemini/skills')); - expect(skillDirs).toHaveLength(0); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', license: '', body: 'Audit body' } - ]; - - transformGemini(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - expect(fs.existsSync(path.join(TEST_DIR, 'gemini-prefixed/.gemini/skills/i-audit/SKILL.md'))).toBe(true); - - const content = fs.readFileSync(path.join(TEST_DIR, 'gemini-prefixed/.gemini/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Ask {{model}} for help.' - } - ]; - - transformGemini(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask Gemini for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'See {{config_file}} for more.' - } - ]; - - transformGemini(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See GEMINI.md for more.'); - }); -}); diff --git a/tests/lib/transformers/kiro.test.js b/tests/lib/transformers/kiro.test.js deleted file mode 100644 index 935e5b0d6..000000000 --- a/tests/lib/transformers/kiro.test.js +++ /dev/null @@ -1,302 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformKiro } from '../../../scripts/lib/transformers/kiro.js'; -import { parseFrontmatter } from '../../../scripts/lib/utils.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-kiro'); - -describe('transformKiro', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - transformKiro([], TEST_DIR); - expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills'))).toBe(true); - }); - - test('should create skill with full frontmatter', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions.' - } - ]; - - transformKiro(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'kiro/.kiro/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.name).toBe('test-skill'); - expect(parsed.frontmatter.description).toBe('A test skill'); - expect(parsed.frontmatter.license).toBe('MIT'); - expect(parsed.body).toBe('Skill instructions.'); - }); - - test('should include compatibility in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - compatibility: 'kiro', - body: 'Body' - } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('compatibility: kiro'); - }); - - test('should include metadata in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - metadata: 'some-metadata', - body: 'Body' - } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('metadata: some-metadata'); - }); - - test('should not include user-invocable in frontmatter (Kiro does not use it)', () => { - const skills = [ - { - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Body' - } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); - expect(content).not.toContain('user-invocable'); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, - { name: 'skill3', description: 'Skill 3', body: 'Body 3' } - ]; - - transformKiro(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/skill2/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/skill3/SKILL.md'))).toBe(true); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Ask {{model}} for help.' - } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask Claude for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'See {{config_file}} for details.' - } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See .kiro/settings.json for details.'); - }); - - test('should replace {{available_commands}} placeholder', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/audit/SKILL.md'), 'utf-8'); - expect(content).toContain('Available: /audit, /polish'); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, - { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } - ] - } - ]; - - transformKiro(skills, TEST_DIR); - - const typoPath = path.join(TEST_DIR, 'kiro/.kiro/skills/frontend-design/reference/typography.md'); - const colorPath = path.join(TEST_DIR, 'kiro/.kiro/skills/frontend-design/reference/color.md'); - - expect(fs.existsSync(typoPath)).toBe(true); - expect(fs.existsSync(colorPath)).toBe(true); - expect(fs.readFileSync(typoPath, 'utf-8')).toBe('Typography reference'); - }); - - test('should replace placeholders in reference files without commandNames', () => { - const skills = [ - { - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Body with {{available_commands}}.', - references: [ - { name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' } - ] - } - ]; - - transformKiro(skills, TEST_DIR); - - const refContent = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/reference/ref.md'), 'utf-8'); - expect(refContent).toContain('Use Claude with .kiro/settings.json.'); - // Reference files should NOT get commandNames, so {{available_commands}} becomes empty string - expect(refContent).toContain('Commands: .'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' } - ]; - - transformKiro(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const outputPath = path.join(TEST_DIR, 'kiro-prefixed/.kiro/skills/i-audit/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should prefix skill references in body when prefix is set', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformKiro(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro-prefixed/.kiro/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('/i-polish'); - expect(content).toContain('the i-audit skill'); - }); - - test('should clean existing directory before writing', () => { - const existingDir = path.join(TEST_DIR, 'kiro/.kiro/skills/old'); - fs.mkdirSync(existingDir, { recursive: true }); - fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); - - const skills = [{ name: 'new', description: 'New', body: 'New' }]; - transformKiro(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/old/SKILL.md'))).toBe(false); - expect(fs.existsSync(path.join(TEST_DIR, 'kiro/.kiro/skills/new/SKILL.md'))).toBe(true); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', userInvocable: false, body: 'body' } - ]; - - transformKiro(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Kiro:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should log reference file count', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Body.', - references: [ - { name: 'ref1', content: 'Ref 1', filePath: '/fake/ref1.md' } - ] - } - ]; - - transformKiro(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 reference files')); - }); - - test('should handle empty skills array', () => { - transformKiro([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'kiro/.kiro/skills')); - expect(skillDirs).toHaveLength(0); - }); - - test('should not include license if empty', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Body' - } - ]; - - transformKiro(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8'); - expect(content).not.toContain('license:'); - }); -}); diff --git a/tests/lib/transformers/opencode.test.js b/tests/lib/transformers/opencode.test.js deleted file mode 100644 index e6b3b558a..000000000 --- a/tests/lib/transformers/opencode.test.js +++ /dev/null @@ -1,395 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformOpenCode } from '../../../scripts/lib/transformers/opencode.js'; -import { parseFrontmatter } from '../../../scripts/lib/utils.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-opencode'); - -describe('transformOpenCode', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - transformOpenCode([], TEST_DIR); - expect(fs.existsSync(path.join(TEST_DIR, 'opencode/.opencode/skills'))).toBe(true); - }); - - test('should create skill with full frontmatter', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'opencode/.opencode/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.name).toBe('test-skill'); - expect(parsed.frontmatter.description).toBe('A test skill'); - expect(parsed.frontmatter.license).toBe('MIT'); - expect(parsed.body).toBe('Skill instructions.'); - }); - - test('should add user-invocable flag for user-invocable skills', () => { - const skills = [ - { - name: 'audit', - description: 'Audit command', - userInvocable: true, - body: 'Audit the code.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/audit/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['user-invocable']).toBe(true); - }); - - test('should not add user-invocable flag for non-user-invocable skills', () => { - const skills = [ - { - name: 'helper', - description: 'Helper skill', - body: 'Helper body.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/helper/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter['user-invocable']).toBeUndefined(); - }); - - test('should include args in frontmatter', () => { - const skills = [ - { - name: 'with-args', - description: 'Command with args', - userInvocable: true, - args: [ - { name: 'target', description: 'Target element', required: false } - ], - body: 'Process {{target}}.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/with-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.args).toBeArray(); - expect(parsed.frontmatter.args).toHaveLength(1); - expect(parsed.frontmatter.args[0].name).toBe('target'); - }); - - test('should not include args when empty', () => { - const skills = [ - { - name: 'no-args', - description: 'No args', - userInvocable: true, - args: [], - body: 'Simple body.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/no-args/SKILL.md'), 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.args).toBeUndefined(); - }); - - test('should include allowed-tools in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - allowedTools: 'Bash,Edit', - body: 'Body' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('allowed-tools: Bash,Edit'); - }); - - test('should include compatibility in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - compatibility: 'opencode', - body: 'Body' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('compatibility: opencode'); - }); - - test('should include metadata in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - metadata: 'some-metadata', - body: 'Body' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('metadata: some-metadata'); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, - { name: 'skill3', description: 'Skill 3', body: 'Body 3' } - ]; - - transformOpenCode(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'opencode/.opencode/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'opencode/.opencode/skills/skill2/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'opencode/.opencode/skills/skill3/SKILL.md'))).toBe(true); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Ask {{model}} for help.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask Claude for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'See {{config_file}} for details.' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See AGENTS.md for details.'); - }); - - test('should replace {{ask_instruction}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'If unclear, {{ask_instruction}}' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('STOP and call the `question` tool to clarify.'); - }); - - test('should replace {{available_commands}} placeholder', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/audit/SKILL.md'), 'utf-8'); - expect(content).toContain('Available: /audit, /polish'); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, - { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } - ] - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const typoPath = path.join(TEST_DIR, 'opencode/.opencode/skills/frontend-design/reference/typography.md'); - const colorPath = path.join(TEST_DIR, 'opencode/.opencode/skills/frontend-design/reference/color.md'); - - expect(fs.existsSync(typoPath)).toBe(true); - expect(fs.existsSync(colorPath)).toBe(true); - expect(fs.readFileSync(typoPath, 'utf-8')).toBe('Typography reference'); - }); - - test('should replace placeholders in reference files without commandNames', () => { - const skills = [ - { - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Body with {{available_commands}}.', - references: [ - { name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' } - ] - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const refContent = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/reference/ref.md'), 'utf-8'); - expect(refContent).toContain('Use Claude with AGENTS.md.'); - // Reference files should NOT get commandNames, so {{available_commands}} becomes empty string - expect(refContent).toContain('Commands: .'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' } - ]; - - transformOpenCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const outputPath = path.join(TEST_DIR, 'opencode-prefixed/.opencode/skills/i-audit/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should prefix skill references in body when prefix is set', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformOpenCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode-prefixed/.opencode/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('/i-polish'); - expect(content).toContain('the i-audit skill'); - }); - - test('should clean existing directory before writing', () => { - const existingDir = path.join(TEST_DIR, 'opencode/.opencode/skills/old'); - fs.mkdirSync(existingDir, { recursive: true }); - fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); - - const skills = [{ name: 'new', description: 'New', body: 'New' }]; - transformOpenCode(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'opencode/.opencode/skills/old/SKILL.md'))).toBe(false); - expect(fs.existsSync(path.join(TEST_DIR, 'opencode/.opencode/skills/new/SKILL.md'))).toBe(true); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', userInvocable: false, body: 'body' } - ]; - - transformOpenCode(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ OpenCode:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should log reference file count', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Body.', - references: [ - { name: 'ref1', content: 'Ref 1', filePath: '/fake/ref1.md' }, - { name: 'ref2', content: 'Ref 2', filePath: '/fake/ref2.md' } - ] - } - ]; - - transformOpenCode(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 reference files')); - }); - - test('should handle empty skills array', () => { - transformOpenCode([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'opencode/.opencode/skills')); - expect(skillDirs).toHaveLength(0); - }); - - test('should not include license if empty', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Body' - } - ]; - - transformOpenCode(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/test/SKILL.md'), 'utf-8'); - expect(content).not.toContain('license:'); - }); -}); diff --git a/tests/lib/transformers/pi.test.js b/tests/lib/transformers/pi.test.js deleted file mode 100644 index 1d4fa87b5..000000000 --- a/tests/lib/transformers/pi.test.js +++ /dev/null @@ -1,302 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import fs from 'fs'; -import path from 'path'; -import { transformPi } from '../../../scripts/lib/transformers/pi.js'; -import { parseFrontmatter } from '../../../scripts/lib/utils.js'; - -const TEST_DIR = path.join(process.cwd(), 'test-tmp-pi'); - -describe('transformPi', () => { - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - afterEach(() => { - if (fs.existsSync(TEST_DIR)) { - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - } - }); - - test('should create correct directory structure', () => { - transformPi([], TEST_DIR); - expect(fs.existsSync(path.join(TEST_DIR, 'pi/.pi/skills'))).toBe(true); - }); - - test('should create skill with full frontmatter', () => { - const skills = [ - { - name: 'test-skill', - description: 'A test skill', - license: 'MIT', - body: 'Skill instructions.' - } - ]; - - transformPi(skills, TEST_DIR); - - const outputPath = path.join(TEST_DIR, 'pi/.pi/skills/test-skill/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - const parsed = parseFrontmatter(content); - - expect(parsed.frontmatter.name).toBe('test-skill'); - expect(parsed.frontmatter.description).toBe('A test skill'); - expect(parsed.frontmatter.license).toBe('MIT'); - expect(parsed.body).toBe('Skill instructions.'); - }); - - test('should include compatibility in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - compatibility: 'pi', - body: 'Body' - } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('compatibility: pi'); - }); - - test('should include metadata in frontmatter', () => { - const skills = [ - { - name: 'test', - description: 'Test', - metadata: 'some-metadata', - body: 'Body' - } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('metadata: some-metadata'); - }); - - test('should not include user-invocable in frontmatter (Pi does not use it)', () => { - const skills = [ - { - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Body' - } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8'); - expect(content).not.toContain('user-invocable'); - }); - - test('should handle multiple skills', () => { - const skills = [ - { name: 'skill1', description: 'Skill 1', body: 'Body 1' }, - { name: 'skill2', description: 'Skill 2', body: 'Body 2' }, - { name: 'skill3', description: 'Skill 3', body: 'Body 3' } - ]; - - transformPi(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'pi/.pi/skills/skill1/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'pi/.pi/skills/skill2/SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(TEST_DIR, 'pi/.pi/skills/skill3/SKILL.md'))).toBe(true); - }); - - test('should replace {{model}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Ask {{model}} for help.' - } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('Ask the model for help.'); - }); - - test('should replace {{config_file}} placeholder', () => { - const skills = [ - { - name: 'test', - description: 'Test', - body: 'See {{config_file}} for details.' - } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8'); - expect(content).toContain('See AGENTS.md for details.'); - }); - - test('should replace {{available_commands}} placeholder', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/audit/SKILL.md'), 'utf-8'); - expect(content).toContain('Available: /audit, /polish'); - }); - - test('should copy reference files', () => { - const skills = [ - { - name: 'frontend-design', - description: 'Design skill', - body: 'Design instructions.', - references: [ - { name: 'typography', content: 'Typography reference', filePath: '/fake/path/typography.md' }, - { name: 'color', content: 'Color reference', filePath: '/fake/path/color.md' } - ] - } - ]; - - transformPi(skills, TEST_DIR); - - const typoPath = path.join(TEST_DIR, 'pi/.pi/skills/frontend-design/reference/typography.md'); - const colorPath = path.join(TEST_DIR, 'pi/.pi/skills/frontend-design/reference/color.md'); - - expect(fs.existsSync(typoPath)).toBe(true); - expect(fs.existsSync(colorPath)).toBe(true); - expect(fs.readFileSync(typoPath, 'utf-8')).toBe('Typography reference'); - }); - - test('should replace placeholders in reference files without commandNames', () => { - const skills = [ - { - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Body with {{available_commands}}.', - references: [ - { name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' } - ] - } - ]; - - transformPi(skills, TEST_DIR); - - const refContent = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/reference/ref.md'), 'utf-8'); - expect(refContent).toContain('Use the model with AGENTS.md.'); - // Reference files should NOT get commandNames, so {{available_commands}} becomes empty string - expect(refContent).toContain('Commands: .'); - }); - - test('should support prefix option', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' } - ]; - - transformPi(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const outputPath = path.join(TEST_DIR, 'pi-prefixed/.pi/skills/i-audit/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - - test('should prefix skill references in body when prefix is set', () => { - const skills = [ - { name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' }, - { name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' } - ]; - - transformPi(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi-prefixed/.pi/skills/i-audit/SKILL.md'), 'utf-8'); - expect(content).toContain('/i-polish'); - expect(content).toContain('the i-audit skill'); - }); - - test('should clean existing directory before writing', () => { - const existingDir = path.join(TEST_DIR, 'pi/.pi/skills/old'); - fs.mkdirSync(existingDir, { recursive: true }); - fs.writeFileSync(path.join(existingDir, 'SKILL.md'), 'old'); - - const skills = [{ name: 'new', description: 'New', body: 'New' }]; - transformPi(skills, TEST_DIR); - - expect(fs.existsSync(path.join(TEST_DIR, 'pi/.pi/skills/old/SKILL.md'))).toBe(false); - expect(fs.existsSync(path.join(TEST_DIR, 'pi/.pi/skills/new/SKILL.md'))).toBe(true); - }); - - test('should log correct summary', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { name: 'skill1', description: 'Test', userInvocable: true, body: 'body' }, - { name: 'skill2', description: 'Test', userInvocable: false, body: 'body' } - ]; - - transformPi(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Pi:')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); - }); - - test('should log reference file count', () => { - const consoleMock = mock(() => {}); - const originalLog = console.log; - console.log = consoleMock; - - const skills = [ - { - name: 'test', - description: 'Test', - body: 'Body.', - references: [ - { name: 'ref1', content: 'Ref 1', filePath: '/fake/ref1.md' } - ] - } - ]; - - transformPi(skills, TEST_DIR); - - console.log = originalLog; - - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 reference files')); - }); - - test('should handle empty skills array', () => { - transformPi([], TEST_DIR); - - const skillDirs = fs.readdirSync(path.join(TEST_DIR, 'pi/.pi/skills')); - expect(skillDirs).toHaveLength(0); - }); - - test('should not include license if empty', () => { - const skills = [ - { - name: 'test', - description: 'Test', - license: '', - body: 'Body' - } - ]; - - transformPi(skills, TEST_DIR); - - const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8'); - expect(content).not.toContain('license:'); - }); -}); diff --git a/tests/lib/transformers/providers.test.js b/tests/lib/transformers/providers.test.js new file mode 100644 index 000000000..67b278856 --- /dev/null +++ b/tests/lib/transformers/providers.test.js @@ -0,0 +1,304 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import fs from 'fs'; +import path from 'path'; +import { PROVIDERS } from '../../../scripts/lib/transformers/providers.js'; +import { createTransformer } from '../../../scripts/lib/transformers/factory.js'; +import { parseFrontmatter, PROVIDER_PLACEHOLDERS } from '../../../scripts/lib/utils.js'; + +const TEST_DIR = path.join(process.cwd(), 'test-tmp-providers'); + +function providerTestDir(provider, suffix = '') { + return path.join(TEST_DIR, `${provider}${suffix}`); +} + +function skillPath(config, skillName, suffix = '') { + return path.join(TEST_DIR, `${config.provider}${suffix}/${config.configDir}/skills/${skillName}/SKILL.md`); +} + +// Test every provider config +for (const [key, config] of Object.entries(PROVIDERS)) { + describe(`Provider: ${config.displayName} (${key})`, () => { + const transform = createTransformer(config); + + beforeEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + afterEach(() => { + if (fs.existsSync(TEST_DIR)) { + fs.rmSync(TEST_DIR, { recursive: true, force: true }); + } + }); + + test('should create correct directory structure', () => { + transform([], TEST_DIR); + expect(fs.existsSync(path.join(TEST_DIR, `${config.provider}/${config.configDir}/skills`))).toBe(true); + }); + + test('should replace {{model}} placeholder correctly', () => { + const skills = [{ name: 'test', description: 'Test', body: 'Ask {{model}} for help.' }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8'); + const expected = PROVIDER_PLACEHOLDERS[config.provider].model; + expect(content).toContain(`Ask ${expected} for help.`); + }); + + test('should replace {{config_file}} placeholder correctly', () => { + const skills = [{ name: 'test', description: 'Test', body: 'See {{config_file}}.' }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8'); + const expected = PROVIDER_PLACEHOLDERS[config.provider].config_file; + expect(content).toContain(`See ${expected}.`); + }); + + test('should support prefix option', () => { + const skills = [{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Body' }]; + transform(skills, TEST_DIR, { prefix: 'i-', outputSuffix: '-prefixed' }); + expect(fs.existsSync(skillPath(config, 'i-audit', '-prefixed'))).toBe(true); + const content = fs.readFileSync(skillPath(config, 'i-audit', '-prefixed'), 'utf-8'); + expect(content).toContain('name: i-audit'); + }); + + test('should copy reference files', () => { + const skills = [{ + name: 'test', + description: 'Test', + body: 'Body', + references: [{ name: 'ref', content: 'Ref content', filePath: '/fake/ref.md' }] + }]; + transform(skills, TEST_DIR); + const refPath = path.join(TEST_DIR, `${config.provider}/${config.configDir}/skills/test/reference/ref.md`); + expect(fs.existsSync(refPath)).toBe(true); + }); + + // Field-specific tests based on provider config + if (config.frontmatterFields.includes('user-invocable')) { + test('should emit user-invocable for user-invocable skills', () => { + const skills = [{ name: 'test', description: 'Test', userInvocable: true, body: 'Body' }]; + transform(skills, TEST_DIR); + const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8')); + expect(parsed.frontmatter['user-invocable']).toBe(true); + }); + + test('should omit user-invocable for non-user-invocable skills', () => { + const skills = [{ name: 'test', description: 'Test', userInvocable: false, body: 'Body' }]; + transform(skills, TEST_DIR); + const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8')); + expect(parsed.frontmatter['user-invocable']).toBeUndefined(); + }); + } + + if (config.frontmatterFields.includes('argument-hint')) { + test('should emit argument-hint for user-invocable skills', () => { + const skills = [{ name: 'test', description: 'Test', userInvocable: true, argumentHint: '[target]', body: 'Body' }]; + transform(skills, TEST_DIR); + const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8')); + expect(parsed.frontmatter['argument-hint']).toBe('[target]'); + }); + + test('should not emit argument-hint for non-user-invocable skills', () => { + const skills = [{ name: 'test', description: 'Test', userInvocable: false, argumentHint: '[target]', body: 'Body' }]; + transform(skills, TEST_DIR); + const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8')); + expect(parsed.frontmatter['argument-hint']).toBeUndefined(); + }); + } + + if (config.frontmatterFields.includes('license')) { + test('should emit license when present', () => { + const skills = [{ name: 'test', description: 'Test', license: 'MIT', body: 'Body' }]; + transform(skills, TEST_DIR); + const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8')); + expect(parsed.frontmatter.license).toBe('MIT'); + }); + + test('should omit license when empty', () => { + const skills = [{ name: 'test', description: 'Test', license: '', body: 'Body' }]; + transform(skills, TEST_DIR); + const parsed = parseFrontmatter(fs.readFileSync(skillPath(config, 'test'), 'utf-8')); + expect(parsed.frontmatter.license).toBeUndefined(); + }); + } + + if (config.frontmatterFields.includes('compatibility')) { + test('should emit compatibility when present', () => { + const skills = [{ name: 'test', description: 'Test', compatibility: config.provider, body: 'Body' }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8'); + expect(content).toContain(`compatibility: ${config.provider}`); + }); + } + + if (config.frontmatterFields.includes('metadata')) { + test('should emit metadata when present', () => { + const skills = [{ name: 'test', description: 'Test', metadata: 'some-metadata', body: 'Body' }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8'); + expect(content).toContain('metadata: some-metadata'); + }); + } + + if (config.frontmatterFields.includes('allowed-tools')) { + test('should emit allowed-tools when present', () => { + const skills = [{ name: 'test', description: 'Test', allowedTools: 'Bash,Edit', body: 'Body' }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8'); + expect(content).toContain('allowed-tools: Bash,Edit'); + }); + } + + // Fields NOT in this provider's allowlist should not appear + const allOptionalFields = ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools']; + const excludedFields = allOptionalFields.filter(f => !config.frontmatterFields.includes(f)); + + if (excludedFields.length > 0) { + test('should not emit fields outside allowlist', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: true, + argumentHint: '[target]', + license: 'MIT', + compatibility: 'all', + metadata: 'meta', + allowedTools: 'Bash', + body: 'Body' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync(skillPath(config, 'test'), 'utf-8'); + + for (const field of excludedFields) { + const yamlKey = field; // field names match yaml keys + // Check the raw content doesn't contain the yaml key + const lines = content.split('\n'); + const frontmatterLines = []; + let inFrontmatter = false; + for (const line of lines) { + if (line === '---') { + if (inFrontmatter) break; + inFrontmatter = true; + continue; + } + if (inFrontmatter) frontmatterLines.push(line); + } + const hasForbiddenField = frontmatterLines.some(l => l.startsWith(`${yamlKey}:`)); + expect(hasForbiddenField).toBe(false); + } + }); + } + }); +} + +// Provider-specific body transform tests +describe('Codex body transform', () => { + const transform = createTransformer(PROVIDERS.codex); + + beforeEach(() => { + if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); + }); + afterEach(() => { + if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); + }); + + test('should transform {{argname}} to $ARGNAME for user-invocable skills', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: true, + argumentHint: '[target]', + body: 'Process {{target}} now.' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync( + path.join(TEST_DIR, `codex/.codex/skills/test/SKILL.md`), 'utf-8' + ); + expect(content).toContain('Process $TARGET now.'); + }); + + test('should not transform {{argname}} for non-user-invocable skills', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: false, + body: 'Process {{target}} now.' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync( + path.join(TEST_DIR, `codex/.codex/skills/test/SKILL.md`), 'utf-8' + ); + // {{target}} should have been left as-is (not transformed by replacePlaceholders either, + // since 'target' is not a known placeholder) + expect(content).toContain('Process {{target}} now.'); + }); + + test('should replace system placeholders before body transform', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: true, + body: 'Ask {{model}} about {{target}}.' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync( + path.join(TEST_DIR, `codex/.codex/skills/test/SKILL.md`), 'utf-8' + ); + // {{model}} replaced first by replacePlaceholders, then {{target}} by bodyTransform + expect(content).toContain('Ask GPT about $TARGET.'); + }); +}); + +describe('Gemini body transform', () => { + const transform = createTransformer(PROVIDERS.gemini); + + beforeEach(() => { + if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); + }); + afterEach(() => { + if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); + }); + + test('should transform all remaining {{*}} to {{args}} for user-invocable skills', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: true, + body: 'Process {{target}} and {{format}}.' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync( + path.join(TEST_DIR, `gemini/.gemini/skills/test/SKILL.md`), 'utf-8' + ); + expect(content).toContain('Process {{args}} and {{args}}.'); + }); + + test('should not transform for non-user-invocable skills', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: false, + body: 'Process {{target}}.' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync( + path.join(TEST_DIR, `gemini/.gemini/skills/test/SKILL.md`), 'utf-8' + ); + expect(content).toContain('Process {{target}}.'); + }); + + test('should replace system placeholders before body transform', () => { + const skills = [{ + name: 'test', + description: 'Test', + userInvocable: true, + body: 'Ask {{model}} about {{target}}.' + }]; + transform(skills, TEST_DIR); + const content = fs.readFileSync( + path.join(TEST_DIR, `gemini/.gemini/skills/test/SKILL.md`), 'utf-8' + ); + // {{model}} replaced first, then remaining {{target}} becomes {{args}} + expect(content).toContain('Ask Gemini about {{args}}.'); + }); +}); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index 81009e66a..898e87b28 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -32,29 +32,18 @@ This is the body content.`; expect(result.body).toBe('This is the body content.'); }); - test('should parse frontmatter with args array', () => { + test('should parse frontmatter with argument-hint', () => { const content = `--- name: test-skill description: A test skill -args: - - name: target - description: The target to normalize - required: false - - name: output - description: Output format - required: true +argument-hint: [TARGET=] --- Body here.`; const result = parseFrontmatter(content); expect(result.frontmatter.name).toBe('test-skill'); - expect(result.frontmatter.args).toBeArray(); - expect(result.frontmatter.args).toHaveLength(2); - expect(result.frontmatter.args[0].name).toBe('target'); - expect(result.frontmatter.args[0].description).toBe('The target to normalize'); - expect(result.frontmatter.args[0].required).toBe(false); - expect(result.frontmatter.args[1].required).toBe(true); + expect(result.frontmatter['argument-hint']).toBe(' [TARGET=]'); }); test('should return empty frontmatter when no frontmatter present', () => { @@ -110,8 +99,8 @@ user-invocable: 'true' Body.`; const result = parseFrontmatter(content); - // The parseFrontmatter function doesn't strip quotes from YAML string values - expect(result.frontmatter['user-invocable']).toBe("'true'"); + // parseFrontmatter strips YAML quotes, so 'true' becomes boolean true + expect(result.frontmatter['user-invocable']).toBe(true); }); test('should parse allowed-tools field', () => { @@ -140,22 +129,15 @@ describe('generateYamlFrontmatter', () => { expect(result).toContain('description: A test'); }); - test('should generate frontmatter with args array', () => { + test('should generate frontmatter with argument-hint', () => { const data = { name: 'test', description: 'Test skill', - args: [ - { name: 'target', description: 'The target', required: false }, - { name: 'output', description: 'Output format', required: true } - ] + 'argument-hint': ' [TARGET=]' }; const result = generateYamlFrontmatter(data); - expect(result).toContain('args:'); - expect(result).toContain('- name: target'); - expect(result).toContain('description: The target'); - expect(result).toContain('required: false'); - expect(result).toContain('required: true'); + expect(result).toContain('argument-hint: [TARGET=]'); }); test('should generate frontmatter with boolean', () => { @@ -173,9 +155,7 @@ describe('generateYamlFrontmatter', () => { const original = { name: 'roundtrip-test', description: 'Testing roundtrip', - args: [ - { name: 'arg1', description: 'First arg', required: true } - ] + 'argument-hint': '' }; const yaml = generateYamlFrontmatter(original); @@ -184,8 +164,7 @@ describe('generateYamlFrontmatter', () => { expect(parsed.frontmatter.name).toBe(original.name); expect(parsed.frontmatter.description).toBe(original.description); - expect(parsed.frontmatter.args).toBeArray(); - expect(parsed.frontmatter.args[0].name).toBe('arg1'); + expect(parsed.frontmatter['argument-hint']).toBe(''); }); });