diff --git a/.claude/agents/documentation-maintainer.md b/.claude/agents/documentation-maintainer.md new file mode 100644 index 0000000..e709cf4 --- /dev/null +++ b/.claude/agents/documentation-maintainer.md @@ -0,0 +1,121 @@ +# Documentation Maintainer Agent + +You are an expert at maintaining technical documentation that is accurate, concise, and optimized for AI context windows. + +## Your Expertise + +- Technical writing best practices +- Context window optimization +- Identifying outdated and redundant documentation +- Documentation structure and organization +- Cross-referencing code and documentation +- Markdown formatting and conventions + +## Your Responsibilities + +### 1. Keep Documentation Accurate + +- Verify line number references are still correct after code changes +- Update configuration examples when options change +- Ensure dependency versions match actual package.json/csproj files +- Remove or update "Recently Fixed" or "Recently Added" sections after releases +- Flag sections that contradict actual code implementation + +### 2. Optimize for Context Windows + +**Remove or relocate:** +- Historical information ("Recently Fixed", "Changelog") +- Future plans and roadmaps (better in GitHub Issues) +- Generic recommendations without specific project context +- Duplicate information available in README or other docs +- Procedural "how to release" steps (better in CONTRIBUTING.md) +- External links and resources (better in README) + +**Keep:** +- Architecture and key components documentation +- Configuration and setup instructions +- Common development tasks with code examples +- Debugging tips specific to this project +- Data flow diagrams and integration patterns +- Important gotchas and edge cases + +### 3. Structure Guidelines + +**CLAUDE.md should focus on:** +- Project architecture and structure +- Key components and their responsibilities +- Configuration options +- Common development tasks +- Debugging tips +- Dependencies + +**README.md should focus on:** +- Installation and quick start +- Features and screenshots +- Basic usage examples +- Links to external resources +- Contributing guidelines +- License and credits + +### 4. Maintenance Tasks + +When asked to review documentation: + +1. **Check for staleness** + - Are versions hardcoded when they shouldn't be? + - Do line number references still match the code? + - Are "recently added" features actually recent? + +2. **Identify redundancy** + - Is this information duplicated in README? + - Are there multiple sections covering the same topic? + - Can bullet lists be condensed? + +3. **Calculate context cost** + - How many lines is this section? + - How frequently is this information needed? + - Could this live in code comments instead? + +4. **Suggest improvements** + - Should this be in GitHub Issues instead? + - Can we reference existing docs instead of duplicating? + - Is this too detailed for overview documentation? + +## Red Flags to Watch For + +- ❌ **"Current Version: X.Y.Z"** in CLAUDE.md - versions should only live in .csproj +- ❌ **"Recently Fixed"** - historical information, bloats context +- ❌ **"Future Enhancements"** - belongs in GitHub Issues +- ❌ **"TODO" lists** - belongs in GitHub Issues or project management +- ❌ **Release procedures** - belongs in CONTRIBUTING.md or CI/CD docs +- ❌ **Generic best practices** - only include project-specific guidance +- ❌ **Outdated line numbers** - verify references are still accurate +- ❌ **Long external link lists** - belongs in README + +## Documentation Review Checklist + +When reviewing CLAUDE.md: + +- [ ] Remove version numbers (except in example code) +- [ ] Remove historical "Recently Fixed" sections +- [ ] Move future plans to GitHub Issues +- [ ] Remove generic recommendations +- [ ] Verify all line number references +- [ ] Check for duplicate information with README +- [ ] Ensure architecture diagrams are still accurate +- [ ] Confirm configuration examples match current code +- [ ] Look for sections that could be condensed +- [ ] Identify content better suited for code comments + +## Optimal CLAUDE.md Size + +Target: **200-300 lines maximum** + +Sections to prioritize: +1. Project Overview (50-75 lines) +2. Key Components (75-100 lines) +3. Configuration (25-50 lines) +4. Common Tasks (25-50 lines) +5. Debugging Tips (25-50 lines) + +If exceeding 350 lines, conduct an audit and remove low-value content. diff --git a/.claude/agents/nuget-package-maintainer.md b/.claude/agents/nuget-package-maintainer.md new file mode 100644 index 0000000..4c15d84 --- /dev/null +++ b/.claude/agents/nuget-package-maintainer.md @@ -0,0 +1,99 @@ +# NuGet Package Maintainer Agent + +You are an expert at maintaining .NET NuGet packages with focus on versioning, releases, and package configuration. + +## Your Expertise + +- Semantic versioning (SemVer) principles and decision-making +- .NET project file (.csproj) configuration +- Central Package Management (Directory.Packages.props) +- NuGet package metadata and best practices +- Release management and changelog generation +- Dependency version management + +## Semantic Versioning Guidelines + +When determining version bumps, analyze changes from the **consumer's perspective**: + +### MAJOR version (X.0.0) +Increment when you make **incompatible API changes**: +- Breaking changes to public APIs +- Removal of public features or endpoints +- Changes to module registration requirements +- Database schema changes requiring manual migration +- Removal of dependencies that consumers might rely on + +### MINOR version (x.Y.0) +Increment when you add **functionality in a backward-compatible manner**: +- New features added +- New UI components or pages +- New configuration options +- Internal implementation changes (e.g., ShadCN → XbyK migration) +- Performance improvements +- Deprecation of features (but not removal) + +### PATCH version (x.y.Z) +Increment when you make **backward-compatible bug fixes**: +- Bug fixes only +- Security patches +- Documentation updates +- Performance fixes that don't change behavior + +## Key Considerations + +1. **Internal vs. External Changes** + - Dependency changes are MINOR if they don't affect consumers + - UI redesigns are MINOR if they don't break integration + - Backend refactors are MINOR if APIs remain the same + +2. **Pre-release Versions** + - Use `-alpha`, `-beta`, `-rc` suffixes for testing + - Example: `2.2.0-beta.1` + +3. **Breaking Change Indicators** + - Does it change how developers install/configure the package? + - Does it change public APIs or data contracts? + - Does it require code changes in consuming applications? + +## Your Tasks + +When asked about versioning: + +1. **Read current version** from `src/XperienceCommunity.Sustainability.csproj` (line 14) +2. **Analyze changes** since last version (review commits, PR descriptions) +3. **Categorize changes**: Breaking, Feature, Fix +4. **Recommend version** with clear reasoning +5. **Update version** in .csproj if requested +6. **Verify consistency**: Check that CLAUDE.md doesn't contain hardcoded versions + +## Package Configuration Checklist + +- `` - Current package version +- `` - Display name +- `<Description>` - Clear, concise package description +- `<Authors>` - Package author(s) +- `<PackageLicenseExpression>` - License (MIT, Apache-2.0, etc.) +- `<PackageIcon>` - Icon file reference +- `<PackageReadmeFile>` - README.md reference +- `<RepositoryUrl>` - GitHub repository URL +- `<PackageTags>` - Searchable tags +- `<GeneratePackageOnBuild>` - Enable for automatic .nupkg generation + +## Release Process + +1. Determine correct version using SemVer guidelines +2. Update version in .csproj +3. Ensure README.md has updated screenshots/features +4. Create release notes summarizing changes +5. Tag release in git: `git tag v2.2.0` +6. Build package: `dotnet build` (auto-generates .nupkg) +7. Publish to NuGet.org or create GitHub release + +## Common Version Decision Examples + +- **Migrating from ShadCN to XbyK components**: MINOR (internal implementation, same public API) +- **Adding new configuration option**: MINOR (new feature, backward-compatible) +- **Fixing percentage calculation bug**: PATCH (bug fix) +- **Removing support for .NET 6**: MAJOR (breaking change) +- **Adding global dashboard feature**: MINOR (new feature) +- **Changing required Kentico version**: MAJOR (breaking compatibility) diff --git a/.claude/agents/playwright-integration.md b/.claude/agents/playwright-integration.md new file mode 100644 index 0000000..556de18 --- /dev/null +++ b/.claude/agents/playwright-integration.md @@ -0,0 +1,358 @@ +# Playwright Integration Agent + +You are an expert at working with Microsoft Playwright for browser automation, testing, and performance measurement in .NET applications. + +## Your Expertise + +- Microsoft.Playwright (.NET) API and best practices +- Browser automation patterns +- Performance measurement and resource tracking +- Memory management and disposal patterns +- CSP (Content Security Policy) handling +- Debugging Playwright issues +- Headless browser optimization + +## Playwright Best Practices + +### 1. Resource Management + +**Always dispose properly:** +```csharp +await using var playwright = await Playwright.CreateAsync(); +await using var browser = await playwright.Chromium.LaunchAsync(); +await using var context = await browser.NewContextAsync(); +await using var page = await context.NewPageAsync(); +``` + +**Critical rules:** +- Use `await using` for all Playwright resources +- Never store browser instances in singletons +- Dispose in reverse order of creation +- Handle disposal in exception scenarios + +### 2. Browser Launch Options + +```csharp +var options = new BrowserTypeLaunchOptions +{ + Headless = true, + // Use custom browser path if needed + ExecutablePath = customBrowserPath +}; +``` + +**Common options:** +- `Headless` - Run without UI (default: true) +- `ExecutablePath` - Custom browser location +- `Args` - Additional browser arguments +- `Timeout` - Launch timeout (default: 30s) + +### 3. Context Configuration + +```csharp +var contextOptions = new BrowserNewContextOptions +{ + BypassCSP = true, // For CSP-protected sites + IgnoreHTTPSErrors = true, // For dev environments + ViewportSize = new ViewportSize { Width = 1920, Height = 1080 } +}; +``` + +**Security considerations:** +- Only use `BypassCSP = true` when necessary +- Only use `IgnoreHTTPSErrors` in dev/test +- Set appropriate viewport size for your use case + +### 4. Page Navigation + +```csharp +// Navigate with timeout +await page.GotoAsync(url, new PageGotoOptions +{ + Timeout = 60000, // 60 seconds + WaitUntil = WaitUntilState.NetworkIdle +}); +``` + +**WaitUntil options:** +- `Load` - Wait for `load` event +- `DOMContentLoaded` - Wait for DOM ready +- `NetworkIdle` - Wait for network to be idle +- `Commit` - Wait for navigation to commit + +### 5. Script Injection + +```csharp +// Add script tag +await page.AddScriptTagAsync(new PageAddScriptTagOptions +{ + Url = "https://cdn.example.com/library.js" +}); + +// Evaluate JavaScript +var result = await page.EvaluateAsync<string>("() => document.title"); + +// Execute script from file +var scriptContent = await File.ReadAllTextAsync("script.js"); +await page.AddScriptTagAsync(new PageAddScriptTagOptions +{ + Content = scriptContent +}); +``` + +### 6. Waiting for Elements + +```csharp +// Wait for selector +var element = await page.WaitForSelectorAsync( + "[data-testid='result']", + new PageWaitForSelectorOptions + { + Timeout = 60000, + State = WaitForSelectorState.Attached + } +); + +// Get text content +var content = await element.TextContentAsync(); +``` + +**Selector strategies:** +- Prefer `data-testid` attributes +- Use CSS selectors for complex queries +- Avoid XPath unless necessary +- Use role-based selectors for accessibility + +## Performance Measurement Patterns + +### Resource Tracking + +```javascript +// In injected script +const resources = performance.getEntriesByType('resource'); +const totalSize = resources.reduce((sum, r) => sum + (r.transferSize || 0), 0); +``` + +**Key metrics:** +- `transferSize` - Actual bytes transferred +- `encodedBodySize` - Compressed size +- `decodedBodySize` - Uncompressed size +- `duration` - Load time + +### Waiting for Resources to Load + +```javascript +// Scroll to trigger lazy loading +window.scrollTo(0, document.body.scrollHeight); + +// Wait for resources +await new Promise(resolve => setTimeout(resolve, 2000)); +``` + +## Error Handling + +### Common Playwright Exceptions + +1. **TimeoutException** - Element/navigation timeout + ```csharp + try + { + await page.WaitForSelectorAsync(selector, new() { Timeout = 30000 }); + } + catch (TimeoutException) + { + // Log and handle gracefully + } + ``` + +2. **TargetClosedException** - Page/browser closed unexpectedly + ```csharp + catch (TargetClosedException) + { + // Browser crashed or closed + } + ``` + +3. **PlaywrightException** - General Playwright errors + ```csharp + catch (PlaywrightException ex) + { + _logger.LogError(ex, "Playwright error: {Message}", ex.Message); + } + ``` + +## Memory and Performance Optimization + +### 1. Browser Instance Management + +**❌ Bad - Memory leak:** +```csharp +private static IBrowser _browser; // Singleton - leaks memory +``` + +**✅ Good - Proper scoping:** +```csharp +public async Task<Result> AnalyzePage(string url) +{ + await using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + // Use and dispose +} +``` + +**🔄 Advanced - Browser pooling:** +For high-frequency operations, consider browser instance pooling with proper lifecycle management. + +### 2. Page Reuse + +**For multiple navigations:** +```csharp +await using var page = await context.NewPageAsync(); + +// Navigate to multiple pages +await page.GotoAsync(url1); +await ProcessPage(page); + +await page.GotoAsync(url2); +await ProcessPage(page); +``` + +### 3. Timeout Configuration + +Make timeouts configurable: +```csharp +var timeout = _options.TimeoutMilliseconds ?? 60000; + +await page.WaitForSelectorAsync( + selector, + new() { Timeout = timeout } +); +``` + +## CSP (Content Security Policy) Handling + +### When CSP Blocks Scripts + +```csharp +var context = await browser.NewContextAsync(new() +{ + BypassCSP = true // Bypasses CSP restrictions +}); +``` + +**Use cases:** +- Injecting analytics/tracking scripts +- Adding third-party libraries via CDN +- Evaluating custom JavaScript on protected sites + +**Security note:** Only use in controlled environments where you trust the content. + +## Debugging Tips + +### 1. Enable Slow Motion + +```csharp +await playwright.Chromium.LaunchAsync(new() +{ + Headless = false, + SlowMo = 500 // 500ms delay between actions +}); +``` + +### 2. Take Screenshots + +```csharp +await page.ScreenshotAsync(new() +{ + Path = "debug.png", + FullPage = true +}); +``` + +### 3. Browser Developer Tools + +```csharp +await playwright.Chromium.LaunchAsync(new() +{ + Headless = false, + Devtools = true // Opens DevTools automatically +}); +``` + +### 4. Verbose Logging + +Set environment variable: +```bash +DEBUG=pw:api +``` + +Or in code: +```csharp +Environment.SetEnvironmentVariable("DEBUG", "pw:api"); +``` + +## Common Patterns for This Project + +### Sustainability Scanning + +1. **Launch browser** (headless Chromium) +2. **Navigate to URL** with network idle wait +3. **Inject resource-checker script** +4. **Wait for results** with timeout +5. **Extract JSON data** from DOM element +6. **Dispose all resources** + +### Error Scenarios + +- **Timeout waiting for data** → Log and return error +- **CSP blocks script** → Use `BypassCSP = true` +- **Page doesn't load** → Check URL accessibility +- **Browser not found** → Ensure `playwright install chromium` ran + +## Installation Requirements + +### Initial Setup + +```bash +# Install browsers +dotnet tool install -g Microsoft.Playwright.CLI +playwright install chromium +``` + +### Deployment Considerations + +- **Docker**: Use official Playwright image +- **Azure/Cloud**: Install browsers in build/startup +- **Windows Server**: May need additional dependencies +- **Linux**: Install browser dependencies via apt + +## Future Optimization Ideas + +1. **Browser pooling** - Reuse browser instances across requests +2. **Page caching** - Cache results for repeat scans +3. **Parallel scanning** - Scan multiple pages concurrently +4. **Incremental updates** - Only rescan changed resources +5. **Browser context sharing** - Share contexts for similar scans + +## Testing Playwright Code + +### Unit Testing + +Mock Playwright interfaces: +```csharp +var mockPage = new Mock<IPage>(); +mockPage.Setup(p => p.WaitForSelectorAsync(...)) + .ReturnsAsync(mockElement.Object); +``` + +### Integration Testing + +Use real Playwright but with test fixtures: +```csharp +[Fact] +public async Task ScanPage_ReturnsValidData() +{ + await using var playwright = await Playwright.CreateAsync(); + // Test with real browser +} +``` diff --git a/.claude/agents/react-admin-ui-developer.md b/.claude/agents/react-admin-ui-developer.md new file mode 100644 index 0000000..3338d17 --- /dev/null +++ b/.claude/agents/react-admin-ui-developer.md @@ -0,0 +1,204 @@ +# React Admin UI Developer Agent + +You are an expert at building and refactoring admin UI components for Xperience by Kentico modules. + +## Your Expertise + +- React 18+ with TypeScript +- XbyK admin framework patterns and conventions +- State management for admin UIs +- Accessibility (WCAG 2.1 AA) +- Responsive design and mobile considerations +- Performance optimization for admin interfaces +- Component composition and reusability + +## XbyK Admin Framework Patterns + +### 1. Page Commands + +Use `usePageCommand` hook for server interactions: + +```typescript +import { usePageCommand } from "@kentico/xperience-admin-base"; + +const { execute: runCommand } = usePageCommand<ResponseType>( + "CommandName", + { + after: (response) => { + // Handle success + }, + onError: (error) => { + // Handle error + }, + } +); +``` + +**Best Practices:** +- Set loading states before calling `execute()` +- Clear errors on new attempts +- Use `inProgress` prop on buttons for loading feedback +- Type the response with proper interface + +### 2. Template Properties + +Admin UI components receive props from backend UIPage: + +```typescript +interface TemplateProps { + pageAvailability: PageAvailabilityStatus; + data: YourDataType; +} + +export const MyTemplate = (props: TemplateProps | null) => { + // Always handle null props + if (!props) return null; + + // Component logic +}; +``` + +### 3. State Management + +For admin UIs, prefer: +- **Local state** with `useState` for UI-only state +- **Props** for server-provided data +- **usePageCommand** for server mutations +- Avoid complex state managers (Redux, MobX) unless absolutely necessary + +## UI/UX Best Practices + +### Loading States + +- Use `inProgress` prop on XbyK Buttons +- Show skeleton loaders for data fetching +- Disable interactive elements during operations +- Provide clear feedback on completion + +### Error Handling + +- Display user-friendly error messages +- Provide actionable recovery steps +- Log detailed errors to console for debugging +- Use XbyK's error UI patterns (colored boxes, icons) + +### Responsive Design + +- Use XbyK's `Row`/`Column` grid system +- Leverage breakpoint props: `colsLg`, `colsMd`, `colsSm` +- Test on mobile viewports +- Ensure touch targets are at least 44x44px + +### Accessibility + +- Use semantic HTML elements +- Provide ARIA labels for icon-only buttons +- Ensure keyboard navigation works +- Maintain color contrast ratios +- Test with screen readers + +## Component Design Patterns + +### Progressive Disclosure + +Show summary first, details on demand: +- Collapsible sections for large datasets +- "Show more" buttons for long lists +- Expandable cards for detailed information + +Example: +```typescript +const [expanded, setExpanded] = useState(false); +const displayCount = expanded ? items.length : 3; + +// Show first 3, with "Show X more" button +``` + +### Dashboard Layouts + +- **Hero section** for primary metric/status +- **Stat cards** for key numbers in grid +- **Detailed breakdowns** below the fold +- **Actions** prominently placed (top-right) + +### Data Visualization + +- Sort by most important metric (size, date, etc.) +- Use color coding for status/severity +- Show percentages and relative values +- Provide context (labels, subtitles) + +## Performance Optimization + +1. **Minimize re-renders** + - Use `React.memo` for expensive components + - Avoid inline function definitions in render + - Use `useCallback` for event handlers passed to children + +2. **Lazy load heavy components** + - Use React.lazy() for code splitting + - Load charts/visualizations only when needed + +3. **Optimize lists** + - Virtualize long lists (react-window) + - Paginate server-side data + - Use `key` prop correctly + +4. **Bundle size** + - Avoid large dependencies + - Use native XbyK components + - Tree-shake unused code + +## Common Patterns for This Project + +### Sustainability Report UI + +**States to handle:** +1. No data + page available → Show "Run Report" CTA +2. No data + page unavailable → Show unavailable message +3. Loading → Show loading state on button +4. Error → Display error with retry option +5. Success → Show comprehensive dashboard + +**Data display:** +- Large, prominent carbon rating with color coding +- Stat cards for key metrics (emissions, size, resources) +- Detailed resource breakdown with expand/collapse +- Actionable optimization tips + +**Interaction patterns:** +- "Run New Analysis" button always visible +- Collapsible resource lists (default: show 3) +- Sort resources by size (largest first) +- File path separation (filename vs. directory) + +## Code Quality Guidelines + +- **TypeScript strict mode** - No implicit `any` +- **Prop interfaces** - Define explicit types +- **Error boundaries** - Catch React errors gracefully +- **Code comments** - Explain complex logic +- **Consistent naming** - Use clear, descriptive names +- **Single responsibility** - Components do one thing well + +## Testing Considerations + +While automated tests don't exist yet, design for testability: +- Pure render functions (no side effects) +- Separate business logic from presentation +- Mock-friendly data structures +- Predictable state updates + +## When to Create Custom Components + +Create custom components when: +- XbyK doesn't provide the pattern +- Reusing across multiple views +- Complex state management needed +- Specific visualization required + +Document custom components with: +- Purpose and use case +- Props interface with JSDoc +- Usage example +- Why XbyK components weren't sufficient diff --git a/.claude/agents/xbyk-component-migration.md b/.claude/agents/xbyk-component-migration.md new file mode 100644 index 0000000..01418dc --- /dev/null +++ b/.claude/agents/xbyk-component-migration.md @@ -0,0 +1,65 @@ +# XbyK Component Migration Agent + +You are an expert at migrating UI components to use native Xperience by Kentico (XbyK) admin components. + +## Your Expertise + +- Deep knowledge of `@kentico/xperience-admin-components` library +- Understanding of XbyK design system, spacing tokens, and layout components +- Experience migrating from third-party UI libraries (ShadCN, Material-UI, etc.) to XbyK +- Bundle size optimization and dependency management +- TypeScript and React best practices for admin UI + +## Your Tasks + +When migrating components: + +1. **Identify third-party dependencies** that can be replaced with native XbyK components +2. **Map components** to XbyK equivalents: + - Buttons → `Button` with `ButtonColor`, `ButtonSize` + - Cards → `Card` with headline/footer props + - Layout → `Stack`, `Row`, `Column` with `Spacing` tokens + - Typography → `Headline` with `HeadlineSize` + - Loading states → Use `inProgress` prop on buttons +3. **Remove unnecessary dependencies** from package.json +4. **Clean up configuration** (tsconfig.json paths, PostCSS, Tailwind, etc.) +5. **Optimize bundle size** by eliminating CSS frameworks and utilities +6. **Maintain accessibility** and responsive design using XbyK's built-in features + +## XbyK Component Reference + +### Core Components +- `Card` - Container with optional headline and footer +- `Button` - Primary UI actions with color/size variants and loading states +- `Stack` - Vertical layout with spacing +- `Row` - Horizontal grid layout +- `Column` - Grid columns with responsive breakpoints (colsLg, colsMd, colsSm) +- `Headline` - Typography with semantic sizing +- `Divider` - Visual separators +- `Paper` - Base container with elevation + +### Spacing Tokens +Use the `Spacing` enum for consistent spacing: +- `Spacing.XS` - 4px +- `Spacing.S` - 8px +- `Spacing.M` - 12px +- `Spacing.L` - 16px +- `Spacing.XL` - 24px +- `Spacing.XXL` - 32px + +### Best Practices +- Always use native XbyK components when available +- Use spacing tokens instead of hardcoded pixel values +- Leverage responsive grid system (Row/Column) +- Use `inProgress` prop for loading states instead of custom spinners +- Avoid inline styles where XbyK component props can handle styling +- Keep bundle size minimal by avoiding external UI libraries + +## When to Create Custom Components + +Only create custom styled components when: +- XbyK doesn't provide the specific UI pattern +- Highly specialized visualization is required +- You need expand/collapse or complex state management + +Always document why custom components are necessary. diff --git a/.gitignore b/.gitignore index fc827a6..6809986 100644 --- a/.gitignore +++ b/.gitignore @@ -419,4 +419,6 @@ FodyWeavers.xsd /examples/DancingGoat/App_Data/playwright # Claude Code -.claude/ \ No newline at end of file +.claude/* +!.claude/commands/ +!.claude/agents/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3cbfccf..ea0cd6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,6 @@ A community-driven open-source NuGet package that brings sustainability insights **Purpose**: Allows content editors to see page weight, carbon emissions, and carbon ratings for individual web pages directly in the Xperience admin UI. -**Current Version**: 2.0.0 **License**: MIT **Repository**: https://github.com/liamgold/xperience-community-sustainability @@ -98,16 +97,26 @@ C:\Projects\xperience-community-sustainability\ ### 3. React Frontend (src/Client/src/Sustainability/SustainabilityTabTemplate.tsx) +**UI Design**: Modern dashboard-style layout using native XbyK components and custom styled components. + **UI States**: -1. **No data + Available**: Shows "Run Sustainability Report" button +1. **No data + Available**: Shows "Run Analysis" button in centered card 2. **No data + Not Available**: Shows unavailable message (root pages/folders) -3. **Data loaded**: Displays report with resource breakdown and carbon rating - -**Features**: -- Loading states with spinner -- Carbon rating colors (A+ green → F red) -- Resource grouping by type (Images, Scripts, CSS, Links, Other) -- "Run again" functionality +3. **Data loaded**: Displays comprehensive dashboard with hero carbon rating + +**Key Features**: +- **Hero Carbon Rating Section** - Large 120px rating letter with gradient background themed by rating color +- **Stat Cards Grid** - 2x2 grid showing CO₂ Emissions, Page Weight, Resources count, and Efficiency rating +- **Collapsible Resource Lists** - Shows 3 resources by default with "Show X more" button +- **Resource Breakdown** - Sorted by size (largest first) with filename/path separation +- **Percentage Badges** - Shows what % of total page weight each resource group represents +- **Optimization Tips** - XbyK-specific features (Image Variants, AIRA) plus general web performance tips +- **Loading states** - Built into XbyK Button component with `inProgress` prop +- **Responsive layout** - Uses XbyK Row/Column with `colsLg`/`colsMd` breakpoints + +**Components Used**: +- XbyK Native: `Card`, `Button`, `Stack`, `Row`, `Column`, `Headline`, `Spacing` +- Custom: `StatCard`, `ResourceGroupCard` (with expand/collapse state) ### 4. JavaScript Analysis (src/wwwroot/scripts/resource-checker.js) @@ -201,23 +210,9 @@ This registers: ## Known Issues & Limitations -### Potential Improvements -1. **External CDN dependency**: Skypack CDN for @tgwf/co2 (availability risk) - consider bundling locally -2. **Commented dashboard**: Dashboard feature is commented out (line 5-12 in SustainabilityDashboard.cs) - planned for future -3. **No automated tests**: Consider adding unit/integration tests - -### Recently Fixed (v2.0.0+) -- ✓ Memory leak with Playwright disposal -- ✓ Service lifetime (Singleton → Scoped) -- ✓ Mixed JSON serializers (now all System.Text.Json) -- ✓ Hardcoded timeout (now configurable) -- ✓ Limited error handling (added comprehensive catch blocks) -- ✓ React error states (added error UI) -- ✓ Naming conventions (DTOs now use PascalCase) -- ✓ Database query optimization -- ✓ Enum extension fragility -- ✓ Null checking for deserialization -- ✓ Magic strings extracted to constants +- **External CDN dependency**: Skypack CDN for @tgwf/co2 (availability risk) - see GitHub issues for planned improvements +- **No automated tests**: Unit/integration tests needed for service and UI components +- **Future enhancements**: See GitHub issues for planned features (global dashboard, historical trends, etc.) ## Development Workflow @@ -236,23 +231,12 @@ dotnet run ### Frontend Development -Client code is built separately (likely via npm/webpack - check Client folder for package.json). - -### Creating a Release - -- Version is set in `XperienceCommunity.Sustainability.csproj` (line 14) -- `GeneratePackageOnBuild` is enabled (line 25) -- NuGet package includes icon, README, and LICENSE - -## Testing Strategy - -**Current state**: No automated tests visible in repository. - -**Recommended additions**: -- Unit tests for `SustainabilityService` (mock Playwright) -- Integration tests for database operations -- E2E tests for admin UI interactions -- Tests for resource-checker.js logic +Client code is built using webpack: +```bash +cd src/Client +npm install +npm run build +``` ## Dependencies @@ -262,9 +246,9 @@ Client code is built separately (likely via npm/webpack - check Client folder fo - `Microsoft.Playwright` (1.52.0) ### NPM Packages (Client/) -- React ecosystem (shadcn/ui components) -- `@kentico/xperience-admin-base` -- `lucide-react` (icons) +- `@kentico/xperience-admin-base` (30.4.2) - Base admin framework +- `@kentico/xperience-admin-components` (30.4.2) - Native XbyK UI components +- React (18.3.1) and React DOM (18.3.1) ### External Runtime - `@tgwf/co2` (v0.15) via Skypack CDN @@ -292,11 +276,6 @@ Update `ResourceGroupType` enum (ExternalResourceGroup.cs:43-55) and `GetInitiat 2. Regenerate `SustainabilityPageDataInfo.generated.cs` (Kentico tooling) 3. Update save/load logic in `SustainabilityService.cs` -## Git Workflow - -**Main branch**: `main` -**Recent work**: UNC path support for shared hosting (commits: 4f08bbd, 2c230ba, fff6239) - ## Debugging Tips 1. **Playwright issues**: Check event log in Kentico admin for logged errors @@ -304,19 +283,3 @@ Update `ResourceGroupType` enum (ExternalResourceGroup.cs:43-55) and `GetInitiat 3. **Timeout errors**: Increase timeout in SustainabilityService.cs:60 or check if page loads slowly 4. **CSP errors**: Ensure `BypassCSP = true` is set (line 45) 5. **Browser not found**: Playwright requires browser installation (`playwright install chromium`) - -## Related Resources - -- [Sustainable Web Design](https://sustainablewebdesign.org/digital-carbon-ratings/) -- [The Green Web Foundation CO2.js](https://developers.thegreenwebfoundation.org/co2js/overview/) -- [Umbraco.Community.Sustainability](https://github.com/umbraco-community/Umbraco.Community.Sustainability) -- [Blog: Bringing Sustainability Insights to Xperience](https://www.goldfinch.me/blog/bringing-sustainability-insights-to-xperience-by-kentico) - -## Future Enhancements - -- Implement global dashboard (currently commented out) -- Historical trend analysis -- Bulk page scanning -- Configurable thresholds and timeouts -- CI/CD pipeline improvements -- Automated testing suite diff --git a/README.md b/README.md index 8765a8a..6887b0d 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ A community-driven open-source package that brings sustainability insights and a Once installed, a new tab appears for each page in your web channels. The Sustainability tab allows content editors and marketers to see and benchmark page weight and carbon emissions, which is then converted to a carbon rating for individual pages. -<a href="/src/images/Sustainability Report - Page Tab.png"> - <img src="/src/images/Sustainability Report - Page Tab.png" width="800" alt="Sustainability Tab for pages in Xperience by Kentico"> +<a href="/src/images/SustainabilityReport-PageTab.jpeg"> + <img src="/src/images/SustainabilityReport-PageTab.jpeg" width="800" alt="Sustainability Tab for pages in Xperience by Kentico"> </a> ## Library Version Matrix diff --git a/src/Client/components/ui/button.tsx b/src/Client/components/ui/button.tsx deleted file mode 100644 index a2df8dc..0000000 --- a/src/Client/components/ui/button.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps<typeof buttonVariants> & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" - - return ( - <Comp - data-slot="button" - className={cn(buttonVariants({ variant, size, className }))} - {...props} - /> - ) -} - -export { Button, buttonVariants } diff --git a/src/Client/components/ui/card.tsx b/src/Client/components/ui/card.tsx deleted file mode 100644 index d05bbc6..0000000 --- a/src/Client/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card" - className={cn( - "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", - className - )} - {...props} - /> - ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-header" - className={cn( - "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", - className - )} - {...props} - /> - ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-title" - className={cn("leading-none font-semibold", className)} - {...props} - /> - ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-description" - className={cn("text-muted-foreground text-sm", className)} - {...props} - /> - ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-action" - className={cn( - "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className - )} - {...props} - /> - ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-content" - className={cn("px-6", className)} - {...props} - /> - ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-footer" - className={cn("flex items-center px-6 [.border-t]:pt-6", className)} - {...props} - /> - ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/src/Client/dist/entry.kxh.03dcdb477d600e65c5c6.js b/src/Client/dist/entry.kxh.03dcdb477d600e65c5c6.js new file mode 100644 index 0000000..ca89395 --- /dev/null +++ b/src/Client/dist/entry.kxh.03dcdb477d600e65c5c6.js @@ -0,0 +1 @@ +System.register(["react","@kentico/xperience-admin-components","@kentico/xperience-admin-base"],(function(e,t){var a={},l={},r={};return{setters:[function(e){a.default=e.default,a.useState=e.useState},function(e){l.Button=e.Button,l.ButtonColor=e.ButtonColor,l.ButtonSize=e.ButtonSize,l.Card=e.Card,l.Column=e.Column,l.Headline=e.Headline,l.HeadlineSize=e.HeadlineSize,l.Row=e.Row,l.Spacing=e.Spacing,l.Stack=e.Stack},function(e){r.usePageCommand=e.usePageCommand}],execute:function(){e((()=>{var e={90:e=>{"use strict";e.exports=r},126:(e,t,a)=>{const l=a(358).y;t.w=function(e){if(e||(e=1),!a.y.meta||!a.y.meta.url)throw console.error("__system_context__",a.y),Error("systemjs-webpack-interop was provided an unknown SystemJS context. Expected context.meta.url, but none was provided");a.p=l(a.y.meta.url,e)}},267:e=>{"use strict";e.exports=l},358:(e,t,a)=>{t.y=function(e,t){var a=document.createElement("a");a.href=e;for(var l="/"===a.pathname[0]?a.pathname:"/"+a.pathname,r=0,n=l.length;r!==t&&n>=0;)"/"===l[--n]&&r++;if(r!==t)throw Error("systemjs-webpack-interop: rootDirectoryLevel ("+t+") is greater than the number of directories ("+r+") in the URL path "+e);var o=l.slice(0,n+1);return a.protocol+"//"+a.host+o};Number.isInteger},726:e=>{"use strict";e.exports=a}},n={};function o(t){var a=n[t];if(void 0!==a)return a.exports;var l=n[t]={exports:{}};return e[t](l,l.exports,o),l.exports}o.y=t,o.d=(e,t)=>{for(var a in t)o.o(t,a)&&!o.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.p="";var i={};return(0,o(126).w)(1),(()=>{"use strict";o.r(i),o.d(i,{SustainabilityDashboardTemplate:()=>t,SustainabilityTabTemplate:()=>f});var e=o(726);const t=({label:t})=>{const[a,l]=(0,e.useState)(t);return e.default.createElement("div",null,e.default.createElement("h1",null,a),e.default.createElement("p",null,"Coming soon: https://github.com/liamgold/xperience-community-sustainability/issues/4."))};var a=o(267),l=o(90),r=function(e){return e[e.Available=0]="Available",e[e.NotAvailable=1]="NotAvailable",e}(r||{});const n={"A+":"Extremely efficient",A:"Very efficient",B:"Efficient",C:"Moderate efficiency",D:"Low efficiency",E:"Poor efficiency",F:"Very poor efficiency"},s={"A+":{primary:"#059669",bg:"#d1fae5",border:"#6ee7b7"},A:{primary:"#16a34a",bg:"#dcfce7",border:"#86efac"},B:{primary:"#65a30d",bg:"#ecfccb",border:"#bef264"},C:{primary:"#ca8a04",bg:"#fef9c3",border:"#fde047"},D:{primary:"#ea580c",bg:"#ffedd5",border:"#fdba74"},E:{primary:"#dc2626",bg:"#fee2e2",border:"#fca5a5"},F:{primary:"#b91c1c",bg:"#fee2e2",border:"#f87171"}},d=({label:t,value:a,subtitle:l})=>e.default.createElement("div",{style:{padding:"20px",background:"white",border:"1px solid #e5e7eb",borderRadius:"8px",boxShadow:"0 1px 2px 0 rgba(0, 0, 0, 0.05)"}},e.default.createElement("div",{style:{fontSize:"13px",fontWeight:600,color:"#6b7280",textTransform:"uppercase",letterSpacing:"0.5px",marginBottom:"8px"}},t),e.default.createElement("div",{style:{fontSize:"28px",fontWeight:700,color:"#111827",marginBottom:l?"4px":"0"}},a),l&&e.default.createElement("div",{style:{fontSize:"12px",color:"#9ca3af"}},l)),c={container:{background:"white",border:"1px solid #e5e7eb",borderRadius:"8px",overflow:"hidden"},header:{padding:"16px 20px",background:"#f9fafb",borderBottom:"1px solid #e5e7eb",display:"flex",justifyContent:"space-between",alignItems:"center"},title:{fontSize:"15px",fontWeight:600,color:"#111827"},subtitle:{fontSize:"13px",color:"#6b7280",marginTop:"2px"},badge:{padding:"4px 12px",background:"#eff6ff",color:"#1e40af",fontSize:"13px",fontWeight:600,borderRadius:"12px"},listContainer:{padding:"12px 20px"},resourceItem:e=>({padding:"12px 0",borderBottom:e?"none":"1px solid #f3f4f6"}),resourceRow:{display:"flex",justifyContent:"space-between",alignItems:"flex-start",gap:"12px"},resourceInfo:{flex:1,minWidth:0},fileName:{fontSize:"13px",fontWeight:500,color:"#111827",marginBottom:"2px",wordBreak:"break-word"},filePath:{fontSize:"12px",color:"#9ca3af",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},fileSize:{fontSize:"13px",fontWeight:600,color:"#6b7280",whiteSpace:"nowrap"},expandButtonContainer:{marginTop:"12px",width:"100%",display:"flex",justifyContent:"center"}},u=({group:t,totalPageSize:l})=>{const[r,n]=(0,e.useState)(!1),o=r?t.resources.length:3;return e.default.createElement("div",{style:c.container},e.default.createElement("div",{style:c.header},e.default.createElement("div",null,e.default.createElement("div",{style:c.title},t.name),e.default.createElement("div",{style:c.subtitle},t.resources.length," resource",1!==t.resources.length?"s":""," •"," ",t.totalSize.toFixed(2)," KB")),e.default.createElement("div",{style:c.badge},(t.totalSize/l*100).toFixed(1),"% of page")),t.resources.length>0&&e.default.createElement("div",{style:c.listContainer},t.resources.slice(0,o).map(((t,a)=>{const l=t.url.split("/").pop()||t.url,r=t.url.substring(0,t.url.lastIndexOf("/")+1),n=a===o-1;return e.default.createElement("div",{key:a,style:c.resourceItem(n)},e.default.createElement("div",{style:c.resourceRow},e.default.createElement("div",{style:c.resourceInfo},e.default.createElement("div",{style:c.fileName},l),e.default.createElement("div",{style:c.filePath,title:r},r)),e.default.createElement("div",{style:c.fileSize},t.size.toFixed(2)," KB")))})),t.resources.length>3&&e.default.createElement("div",{style:c.expandButtonContainer},e.default.createElement(a.Button,{label:r?"Show less":`Show ${t.resources.length-3} more`,onClick:()=>n(!r),color:a.ButtonColor.Secondary,size:a.ButtonSize.S}))))},f=t=>{const[o,i]=(0,e.useState)(!1),[c,f]=(0,e.useState)(null),[p,m]=(0,e.useState)(t?.sustainabilityData),{execute:g}=(0,l.usePageCommand)("RunReport",{after:e=>{m(e?.sustainabilityData),i(!1),f(null)},onError:e=>{i(!1),f("Failed to run sustainability report. Please try again."),console.error("Sustainability report error:",e)}});if(null==p)return e.default.createElement("div",{style:{padding:"32px",maxWidth:"1400px",margin:"0 auto"}},e.default.createElement(a.Stack,{spacing:a.Spacing.XL},e.default.createElement("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"}},e.default.createElement(a.Headline,{size:a.HeadlineSize.L},"Sustainability Report")),e.default.createElement("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"400px"}},e.default.createElement(a.Card,{headline:"No Data Available",fullHeight:!1},e.default.createElement(a.Stack,{spacing:a.Spacing.L},e.default.createElement("p",{style:{fontSize:"14px",color:"#6b7280"}},t?.pageAvailability===r.Available?"Run a sustainability analysis to see your page's environmental impact.":"This page is not available for analysis."),c&&e.default.createElement("div",{style:{padding:"12px 16px",background:"#fef2f2",border:"1px solid #fecaca",borderRadius:"6px",fontSize:"13px",color:"#dc2626"}},c),t?.pageAvailability===r.Available&&e.default.createElement(a.Button,{label:"Run Analysis",color:a.ButtonColor.Primary,size:a.ButtonSize.L,disabled:o,inProgress:o,onClick:()=>{i(!0),f(null),g()}}))))));const b=s[p.carbonRating]||s.C,y=p.resourceGroups.reduce(((e,t)=>e+t.resources.length),0);return e.default.createElement("div",{style:{padding:"32px",maxWidth:"1400px",margin:"0 auto"}},e.default.createElement(a.Stack,{spacing:a.Spacing.XL},e.default.createElement("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",flexWrap:"wrap",gap:"16px"}},e.default.createElement("div",null,e.default.createElement(a.Headline,{size:a.HeadlineSize.L},"Sustainability Report"),e.default.createElement("div",{style:{fontSize:"14px",color:"#6b7280",marginTop:"4px"}},"Last analyzed: ",p.lastRunDate)),e.default.createElement(a.Button,{label:"Run New Analysis",color:a.ButtonColor.Primary,size:a.ButtonSize.M,disabled:o,inProgress:o,onClick:()=>{i(!0),f(null),g()}})),c&&e.default.createElement("div",{style:{padding:"16px",background:"#fef2f2",border:"1px solid #fecaca",borderRadius:"8px",fontSize:"14px",color:"#dc2626"}},c),e.default.createElement("div",{style:{background:`linear-gradient(135deg, ${b.bg} 0%, white 100%)`,border:`2px solid ${b.border}`,borderRadius:"12px",padding:"40px",position:"relative",overflow:"hidden"}},e.default.createElement("div",{style:{position:"absolute",top:"-50px",right:"-50px",width:"200px",height:"200px",background:b.bg,borderRadius:"50%",opacity:.3}}),e.default.createElement(a.Row,{spacing:a.Spacing.XL},e.default.createElement(a.Column,{colsLg:6,colsMd:12},e.default.createElement("div",{style:{position:"relative",zIndex:1}},e.default.createElement("div",{style:{fontSize:"14px",fontWeight:600,color:b.primary,textTransform:"uppercase",letterSpacing:"1px",marginBottom:"12px"}},"Carbon Rating"),e.default.createElement("div",{style:{fontSize:"120px",fontWeight:900,color:b.primary,lineHeight:1,marginBottom:"16px",textShadow:`0 2px 8px ${b.bg}`}},p.carbonRating),e.default.createElement("div",{style:{fontSize:"18px",fontWeight:600,color:"#111827",marginBottom:"8px"}},n[p.carbonRating]),e.default.createElement("div",{style:{fontSize:"14px",color:"#6b7280"}},"A+"===p.carbonRating||"A"===p.carbonRating?"This page has excellent carbon efficiency.":"B"===p.carbonRating||"C"===p.carbonRating?"This page has room for improvement.":"This page needs significant optimization."))),e.default.createElement(a.Column,{colsLg:6,colsMd:12},e.default.createElement(a.Row,{spacing:a.Spacing.L,spacingY:a.Spacing.L},e.default.createElement(a.Column,{colsLg:6,colsMd:6},e.default.createElement(d,{label:"CO₂ Emissions",value:`${p.totalEmissions.toFixed(3)}g`,subtitle:"per page view"})),e.default.createElement(a.Column,{colsLg:6,colsMd:6},e.default.createElement(d,{label:"Page Weight",value:`${(p.totalSize/1024).toFixed(2)}MB`,subtitle:`${p.totalSize.toFixed(0)} KB total`})),e.default.createElement(a.Column,{colsLg:6,colsMd:6},e.default.createElement(d,{label:"Resources",value:`${y}`,subtitle:`${p.resourceGroups.length} categories`})),e.default.createElement(a.Column,{colsLg:6,colsMd:6},e.default.createElement(d,{label:"Efficiency",value:p.totalEmissions<.1?"Excellent":p.totalEmissions<.2?"Good":p.totalEmissions<.3?"Fair":"Poor",subtitle:"Overall rating"})))))),e.default.createElement("div",null,e.default.createElement(a.Headline,{size:a.HeadlineSize.M,spacingBottom:a.Spacing.L},"Resource Breakdown"),e.default.createElement(a.Stack,{spacing:a.Spacing.L},p.resourceGroups.sort(((e,t)=>t.totalSize-e.totalSize)).map((t=>e.default.createElement(u,{key:t.type,group:t,totalPageSize:p.totalSize}))))),e.default.createElement("div",{style:{padding:"24px",background:"#f0f9ff",border:"1px solid #bae6fd",borderRadius:"8px"}},e.default.createElement("div",{style:{fontSize:"15px",fontWeight:600,color:"#0c4a6e",marginBottom:"12px"}},"💡 Tips to improve your carbon footprint"),e.default.createElement("ul",{style:{margin:0,paddingLeft:"20px",color:"#075985"}},e.default.createElement("li",{style:{marginBottom:"6px"}},e.default.createElement("strong",null,"Use Image Variants")," - Configure responsive image variants with specific dimensions and aspect ratios to serve optimized versions for different contexts (hero banners, thumbnails, social media)"),e.default.createElement("li",{style:{marginBottom:"6px"}},e.default.createElement("strong",null,"Enable AIRA's Smart Optimization")," - Leverage AIRA's AI-powered features for automatic image format conversion, smart focal point detection, and automated quality optimization during uploads"),e.default.createElement("li",{style:{marginBottom:"6px"}},e.default.createElement("strong",null,"Automate Image Metadata")," - Use AIRA to automatically generate alt texts, descriptions, and tags for better SEO while reducing manual work"),e.default.createElement("li",{style:{marginBottom:"6px"}},e.default.createElement("strong",null,"Minimize CSS & JavaScript")," - Bundle and minify your assets to reduce file sizes and decrease the number of HTTP requests"),e.default.createElement("li",{style:{marginBottom:"6px"}},e.default.createElement("strong",null,"Enable Browser Caching")," - Configure cache headers for static resources to reduce repeat downloads and server load"),e.default.createElement("li",null,e.default.createElement("strong",null,"Implement Lazy Loading")," - Load images and resources only when they're needed, improving initial page load performance")))))}})(),i})())}}})); \ No newline at end of file diff --git a/src/Client/dist/entry.kxh.312f02820374d69a4ac8.js b/src/Client/dist/entry.kxh.312f02820374d69a4ac8.js deleted file mode 100644 index accba69..0000000 --- a/src/Client/dist/entry.kxh.312f02820374d69a4ac8.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! For license information please see entry.kxh.312f02820374d69a4ac8.js.LICENSE.txt */ -System.register(["react","@kentico/xperience-admin-base"],(function(e,n){var r={},t={};return Object.defineProperty(r,"__esModule",{value:!0}),{setters:[function(e){Object.keys(e).forEach((function(n){r[n]=e[n]}))},function(e){t.usePageCommand=e.usePageCommand}],execute:function(){e((()=>{var e={20:(e,n,r)=>{"use strict";var t=r(726),a=Symbol.for("react.element"),o=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};n.jsx=function(e,n,r){var t,l={},c=null,d=null;for(t in void 0!==r&&(c=""+r),void 0!==n.key&&(c=""+n.key),void 0!==n.ref&&(d=n.ref),n)o.call(n,t)&&!s.hasOwnProperty(t)&&(l[t]=n[t]);if(e&&e.defaultProps)for(t in n=e.defaultProps)void 0===l[t]&&(l[t]=n[t]);return{$$typeof:a,type:e,key:c,ref:d,props:l,_owner:i.current}}},56:(e,n,r)=>{"use strict";e.exports=function(e){var n=r.nc;n&&e.setAttribute("nonce",n)}},72:e=>{"use strict";var n=[];function r(e){for(var r=-1,t=0;t<n.length;t++)if(n[t].identifier===e){r=t;break}return r}function t(e,t){for(var o={},i=[],s=0;s<e.length;s++){var l=e[s],c=t.base?l[0]+t.base:l[0],d=o[c]||0,p="".concat(c," ").concat(d);o[c]=d+1;var u=r(p),m={css:l[1],media:l[2],sourceMap:l[3],supports:l[4],layer:l[5]};if(-1!==u)n[u].references++,n[u].updater(m);else{var b=a(m,t);t.byIndex=s,n.splice(s,0,{identifier:p,updater:b,references:1})}i.push(p)}return i}function a(e,n){var r=n.domAPI(n);return r.update(e),function(n){if(n){if(n.css===e.css&&n.media===e.media&&n.sourceMap===e.sourceMap&&n.supports===e.supports&&n.layer===e.layer)return;r.update(e=n)}else r.remove()}}e.exports=function(e,a){var o=t(e=e||[],a=a||{});return function(e){e=e||[];for(var i=0;i<o.length;i++){var s=r(o[i]);n[s].references--}for(var l=t(e,a),c=0;c<o.length;c++){var d=r(o[c]);0===n[d].references&&(n[d].updater(),n.splice(d,1))}o=l}}},90:e=>{"use strict";e.exports=t},113:e=>{"use strict";e.exports=function(e,n){if(n.styleSheet)n.styleSheet.cssText=e;else{for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(document.createTextNode(e))}}},126:(e,n,r)=>{const t=r(358).y;n.w=function(e){if(e||(e=1),!r.y.meta||!r.y.meta.url)throw console.error("__system_context__",r.y),Error("systemjs-webpack-interop was provided an unknown SystemJS context. Expected context.meta.url, but none was provided");r.p=t(r.y.meta.url,e)}},314:e=>{"use strict";e.exports=function(e){var n=[];return n.toString=function(){return this.map((function(n){var r="",t=void 0!==n[5];return n[4]&&(r+="@supports (".concat(n[4],") {")),n[2]&&(r+="@media ".concat(n[2]," {")),t&&(r+="@layer".concat(n[5].length>0?" ".concat(n[5]):""," {")),r+=e(n),t&&(r+="}"),n[2]&&(r+="}"),n[4]&&(r+="}"),r})).join("")},n.i=function(e,r,t,a,o){"string"==typeof e&&(e=[[null,e,void 0]]);var i={};if(t)for(var s=0;s<this.length;s++){var l=this[s][0];null!=l&&(i[l]=!0)}for(var c=0;c<e.length;c++){var d=[].concat(e[c]);t&&i[d[0]]||(void 0!==o&&(void 0===d[5]||(d[1]="@layer".concat(d[5].length>0?" ".concat(d[5]):""," {").concat(d[1],"}")),d[5]=o),r&&(d[2]?(d[1]="@media ".concat(d[2]," {").concat(d[1],"}"),d[2]=r):d[2]=r),a&&(d[4]?(d[1]="@supports (".concat(d[4],") {").concat(d[1],"}"),d[4]=a):d[4]="".concat(a)),n.push(d))}},n}},358:(e,n,r)=>{n.y=function(e,n){var r=document.createElement("a");r.href=e;for(var t="/"===r.pathname[0]?r.pathname:"/"+r.pathname,a=0,o=t.length;a!==n&&o>=0;)"/"===t[--o]&&a++;if(a!==n)throw Error("systemjs-webpack-interop: rootDirectoryLevel ("+n+") is greater than the number of directories ("+a+") in the URL path "+e);var i=t.slice(0,o+1);return r.protocol+"//"+r.host+i};Number.isInteger},497:(e,n,r)=>{"use strict";r.d(n,{A:()=>s});var t=r(601),a=r.n(t),o=r(314),i=r.n(o)()(a());i.push([e.id,'/*! tailwindcss v4.1.6 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",\n "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",\n "Courier New", monospace;\n --color-red-50: oklch(97.1% 0.013 17.38);\n --color-red-200: oklch(88.5% 0.062 18.334);\n --color-red-500: oklch(63.7% 0.237 25.331);\n --color-red-600: oklch(57.7% 0.245 27.325);\n --color-red-700: oklch(50.5% 0.213 27.518);\n --color-orange-600: oklch(64.6% 0.222 41.116);\n --color-amber-600: oklch(66.6% 0.179 58.318);\n --color-yellow-600: oklch(68.1% 0.162 75.834);\n --color-lime-600: oklch(64.8% 0.2 131.684);\n --color-green-600: oklch(62.7% 0.194 149.214);\n --color-emerald-600: oklch(59.6% 0.145 163.225);\n --color-white: #fff;\n --spacing: 0.25rem;\n --container-sm: 24rem;\n --container-md: 28rem;\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-tight: -0.025em;\n --animate-spin: spin 1s linear infinite;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden="until-found"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .\\@container\\/card-header {\n container-type: inline-size;\n container-name: card-header;\n }\n .collapse {\n visibility: collapse;\n }\n .invisible {\n visibility: hidden;\n }\n .visible {\n visibility: visible;\n }\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n }\n .not-sr-only {\n position: static;\n width: auto;\n height: auto;\n padding: 0;\n margin: 0;\n overflow: visible;\n clip: auto;\n white-space: normal;\n }\n .absolute {\n position: absolute;\n }\n .fixed {\n position: fixed;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .sticky {\n position: sticky;\n }\n .isolate {\n isolation: isolate;\n }\n .isolation-auto {\n isolation: auto;\n }\n .col-start-2 {\n grid-column-start: 2;\n }\n .row-span-2 {\n grid-row: span 2 / span 2;\n }\n .row-start-1 {\n grid-row-start: 1;\n }\n .container {\n width: 100%;\n @media (width >= 40rem) {\n max-width: 40rem;\n }\n @media (width >= 48rem) {\n max-width: 48rem;\n }\n @media (width >= 64rem) {\n max-width: 64rem;\n }\n @media (width >= 80rem) {\n max-width: 80rem;\n }\n @media (width >= 96rem) {\n max-width: 96rem;\n }\n }\n .block {\n display: block;\n }\n .contents {\n display: contents;\n }\n .flex {\n display: flex;\n }\n .flow-root {\n display: flow-root;\n }\n .grid {\n display: grid;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .inline-grid {\n display: inline-grid;\n }\n .inline-table {\n display: inline-table;\n }\n .list-item {\n display: list-item;\n }\n .table {\n display: table;\n }\n .table-caption {\n display: table-caption;\n }\n .table-cell {\n display: table-cell;\n }\n .table-column {\n display: table-column;\n }\n .table-column-group {\n display: table-column-group;\n }\n .table-footer-group {\n display: table-footer-group;\n }\n .table-header-group {\n display: table-header-group;\n }\n .table-row {\n display: table-row;\n }\n .table-row-group {\n display: table-row-group;\n }\n .size-9 {\n width: calc(var(--spacing) * 9);\n height: calc(var(--spacing) * 9);\n }\n .h-8 {\n height: calc(var(--spacing) * 8);\n }\n .h-9 {\n height: calc(var(--spacing) * 9);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .min-h-\\[60vh\\] {\n min-height: 60vh;\n }\n .w-full {\n width: 100%;\n }\n .max-w-md {\n max-width: var(--container-md);\n }\n .max-w-sm {\n max-width: var(--container-sm);\n }\n .flex-shrink {\n flex-shrink: 1;\n }\n .shrink {\n flex-shrink: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .flex-grow {\n flex-grow: 1;\n }\n .grow {\n flex-grow: 1;\n }\n .border-collapse {\n border-collapse: collapse;\n }\n .translate-none {\n translate: none;\n }\n .scale-3d {\n scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z);\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .touch-pinch-zoom {\n --tw-pinch-zoom: pinch-zoom;\n touch-action: var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,);\n }\n .resize {\n resize: both;\n }\n .list-inside {\n list-style-position: inside;\n }\n .list-disc {\n list-style-type: disc;\n }\n .auto-rows-min {\n grid-auto-rows: min-content;\n }\n .grid-cols-1 {\n grid-template-columns: repeat(1, minmax(0, 1fr));\n }\n .grid-rows-\\[auto_auto\\] {\n grid-template-rows: auto auto;\n }\n .flex-col {\n flex-direction: column;\n }\n .flex-wrap {\n flex-wrap: wrap;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-center {\n justify-content: center;\n }\n .gap-1 {\n gap: calc(var(--spacing) * 1);\n }\n .gap-1\\.5 {\n gap: calc(var(--spacing) * 1.5);\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-6 {\n gap: calc(var(--spacing) * 6);\n }\n .space-y-0 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-0\\.5 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0.5) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0.5) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-1 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-3 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-4 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-reverse {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 1;\n }\n }\n .space-x-reverse {\n :where(& > :not(:last-child)) {\n --tw-space-x-reverse: 1;\n }\n }\n .divide-x {\n :where(& > :not(:last-child)) {\n --tw-divide-x-reverse: 0;\n border-inline-style: var(--tw-border-style);\n border-inline-start-width: calc(1px * var(--tw-divide-x-reverse));\n border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));\n }\n }\n .divide-y {\n :where(& > :not(:last-child)) {\n --tw-divide-y-reverse: 0;\n border-bottom-style: var(--tw-border-style);\n border-top-style: var(--tw-border-style);\n border-top-width: calc(1px * var(--tw-divide-y-reverse));\n border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));\n }\n }\n .divide-y-reverse {\n :where(& > :not(:last-child)) {\n --tw-divide-y-reverse: 1;\n }\n }\n .self-start {\n align-self: flex-start;\n }\n .justify-self-end {\n justify-self: flex-end;\n }\n .truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-md {\n border-radius: calc(var(--radius) - 2px);\n }\n .rounded-xl {\n border-radius: calc(var(--radius) + 4px);\n }\n .rounded-s {\n border-start-start-radius: 0.25rem;\n border-end-start-radius: 0.25rem;\n }\n .rounded-ss {\n border-start-start-radius: 0.25rem;\n }\n .rounded-e {\n border-start-end-radius: 0.25rem;\n border-end-end-radius: 0.25rem;\n }\n .rounded-se {\n border-start-end-radius: 0.25rem;\n }\n .rounded-ee {\n border-end-end-radius: 0.25rem;\n }\n .rounded-es {\n border-end-start-radius: 0.25rem;\n }\n .rounded-t {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n }\n .rounded-l {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n }\n .rounded-tl {\n border-top-left-radius: 0.25rem;\n }\n .rounded-r {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n }\n .rounded-tr {\n border-top-right-radius: 0.25rem;\n }\n .rounded-b {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n }\n .rounded-br {\n border-bottom-right-radius: 0.25rem;\n }\n .rounded-bl {\n border-bottom-left-radius: 0.25rem;\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-x {\n border-inline-style: var(--tw-border-style);\n border-inline-width: 1px;\n }\n .border-y {\n border-block-style: var(--tw-border-style);\n border-block-width: 1px;\n }\n .border-s {\n border-inline-start-style: var(--tw-border-style);\n border-inline-start-width: 1px;\n }\n .border-e {\n border-inline-end-style: var(--tw-border-style);\n border-inline-end-width: 1px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-r {\n border-right-style: var(--tw-border-style);\n border-right-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-red-200 {\n border-color: var(--color-red-200);\n }\n .bg-background {\n background-color: var(--background);\n }\n .bg-card {\n background-color: var(--card);\n }\n .bg-destructive {\n background-color: var(--destructive);\n }\n .bg-primary {\n background-color: var(--primary);\n }\n .bg-red-50 {\n background-color: var(--color-red-50);\n }\n .bg-secondary {\n background-color: var(--secondary);\n }\n .bg-repeat {\n background-repeat: repeat;\n }\n .mask-no-clip {\n mask-clip: no-clip;\n }\n .mask-repeat {\n mask-repeat: repeat;\n }\n .p-3 {\n padding: calc(var(--spacing) * 3);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .px-6 {\n padding-inline: calc(var(--spacing) * 6);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-6 {\n padding-block: calc(var(--spacing) * 6);\n }\n .text-center {\n text-align: center;\n }\n .text-2xl {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n .text-3xl {\n font-size: var(--text-3xl);\n line-height: var(--tw-leading, var(--text-3xl--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .leading-none {\n --tw-leading: 1;\n line-height: 1;\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-tight {\n --tw-tracking: var(--tracking-tight);\n letter-spacing: var(--tracking-tight);\n }\n .text-wrap {\n text-wrap: wrap;\n }\n .text-clip {\n text-overflow: clip;\n }\n .text-ellipsis {\n text-overflow: ellipsis;\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .text-amber-600 {\n color: var(--color-amber-600);\n }\n .text-card-foreground {\n color: var(--card-foreground);\n }\n .text-emerald-600 {\n color: var(--color-emerald-600);\n }\n .text-foreground {\n color: var(--foreground);\n }\n .text-green-600 {\n color: var(--color-green-600);\n }\n .text-lime-600 {\n color: var(--color-lime-600);\n }\n .text-muted-foreground {\n color: var(--muted-foreground);\n }\n .text-orange-600 {\n color: var(--color-orange-600);\n }\n .text-primary {\n color: var(--primary);\n }\n .text-primary-foreground {\n color: var(--primary-foreground);\n }\n .text-red-500 {\n color: var(--color-red-500);\n }\n .text-red-600 {\n color: var(--color-red-600);\n }\n .text-red-700 {\n color: var(--color-red-700);\n }\n .text-secondary-foreground {\n color: var(--secondary-foreground);\n }\n .text-white {\n color: var(--color-white);\n }\n .text-yellow-600 {\n color: var(--color-yellow-600);\n }\n .capitalize {\n text-transform: capitalize;\n }\n .lowercase {\n text-transform: lowercase;\n }\n .normal-case {\n text-transform: none;\n }\n .uppercase {\n text-transform: uppercase;\n }\n .italic {\n font-style: italic;\n }\n .not-italic {\n font-style: normal;\n }\n .diagonal-fractions {\n --tw-numeric-fraction: diagonal-fractions;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .lining-nums {\n --tw-numeric-figure: lining-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .oldstyle-nums {\n --tw-numeric-figure: oldstyle-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .ordinal {\n --tw-ordinal: ordinal;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .proportional-nums {\n --tw-numeric-spacing: proportional-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .slashed-zero {\n --tw-slashed-zero: slashed-zero;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .stacked-fractions {\n --tw-numeric-fraction: stacked-fractions;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .tabular-nums {\n --tw-numeric-spacing: tabular-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n }\n .normal-nums {\n font-variant-numeric: normal;\n }\n .line-through {\n text-decoration-line: line-through;\n }\n .no-underline {\n text-decoration-line: none;\n }\n .overline {\n text-decoration-line: overline;\n }\n .underline {\n text-decoration-line: underline;\n }\n .underline-offset-4 {\n text-underline-offset: 4px;\n }\n .antialiased {\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n .subpixel-antialiased {\n -webkit-font-smoothing: auto;\n -moz-osx-font-smoothing: auto;\n }\n .shadow {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-sm {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-xs {\n --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .inset-ring {\n --tw-inset-ring-shadow: inset 0 0 0 1px var(--tw-inset-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .outline {\n outline-style: var(--tw-outline-style);\n outline-width: 1px;\n }\n .blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n }\n .drop-shadow {\n --tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.1))) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.06)));\n --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06));\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n }\n .filter {\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n }\n .backdrop-blur {\n --tw-backdrop-blur: blur(8px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-grayscale {\n --tw-backdrop-grayscale: grayscale(100%);\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-invert {\n --tw-backdrop-invert: invert(100%);\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-sepia {\n --tw-backdrop-sepia: sepia(100%);\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-filter {\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .outline-none {\n --tw-outline-style: none;\n outline-style: none;\n }\n .divide-x-reverse {\n :where(& > :not(:last-child)) {\n --tw-divide-x-reverse: 1;\n }\n }\n .ring-inset {\n --tw-ring-inset: inset;\n }\n .running {\n animation-play-state: running;\n }\n .zoom-in {\n --tw-enter-scale: 0;\n }\n .zoom-out {\n --tw-exit-scale: 0;\n }\n .hover\\:bg-accent {\n &:hover {\n @media (hover: hover) {\n background-color: var(--accent);\n }\n }\n }\n .hover\\:bg-destructive\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--destructive);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--destructive) 90%, transparent);\n }\n }\n }\n }\n .hover\\:bg-primary\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--primary);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--primary) 90%, transparent);\n }\n }\n }\n }\n .hover\\:bg-secondary\\/80 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--secondary);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--secondary) 80%, transparent);\n }\n }\n }\n }\n .hover\\:text-accent-foreground {\n &:hover {\n @media (hover: hover) {\n color: var(--accent-foreground);\n }\n }\n }\n .hover\\:underline {\n &:hover {\n @media (hover: hover) {\n text-decoration-line: underline;\n }\n }\n }\n .focus-visible\\:border-ring {\n &:focus-visible {\n border-color: var(--ring);\n }\n }\n .focus-visible\\:ring-\\[3px\\] {\n &:focus-visible {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus-visible\\:ring-destructive\\/20 {\n &:focus-visible {\n --tw-ring-color: var(--destructive);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent);\n }\n }\n }\n .focus-visible\\:ring-ring\\/50 {\n &:focus-visible {\n --tw-ring-color: var(--ring);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-ring-color: color-mix(in oklab, var(--ring) 50%, transparent);\n }\n }\n }\n .disabled\\:pointer-events-none {\n &:disabled {\n pointer-events: none;\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n .has-data-\\[slot\\=card-action\\]\\:grid-cols-\\[1fr_auto\\] {\n &:has(*[data-slot="card-action"]) {\n grid-template-columns: 1fr auto;\n }\n }\n .has-\\[\\>svg\\]\\:px-2\\.5 {\n &:has(>svg) {\n padding-inline: calc(var(--spacing) * 2.5);\n }\n }\n .has-\\[\\>svg\\]\\:px-3 {\n &:has(>svg) {\n padding-inline: calc(var(--spacing) * 3);\n }\n }\n .has-\\[\\>svg\\]\\:px-4 {\n &:has(>svg) {\n padding-inline: calc(var(--spacing) * 4);\n }\n }\n .aria-invalid\\:border-destructive {\n &[aria-invalid="true"] {\n border-color: var(--destructive);\n }\n }\n .aria-invalid\\:ring-destructive\\/20 {\n &[aria-invalid="true"] {\n --tw-ring-color: var(--destructive);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-ring-color: color-mix(in oklab, var(--destructive) 20%, transparent);\n }\n }\n }\n .md\\:col-span-2 {\n @media (width >= 48rem) {\n grid-column: span 2 / span 2;\n }\n }\n .md\\:grid-cols-3 {\n @media (width >= 48rem) {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n }\n .dark\\:border-input {\n &:is(.dark *) {\n border-color: var(--input);\n }\n }\n .dark\\:bg-destructive\\/60 {\n &:is(.dark *) {\n background-color: var(--destructive);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--destructive) 60%, transparent);\n }\n }\n }\n .dark\\:bg-input\\/30 {\n &:is(.dark *) {\n background-color: var(--input);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--input) 30%, transparent);\n }\n }\n }\n .dark\\:hover\\:bg-accent\\/50 {\n &:is(.dark *) {\n &:hover {\n @media (hover: hover) {\n background-color: var(--accent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--accent) 50%, transparent);\n }\n }\n }\n }\n }\n .dark\\:hover\\:bg-input\\/50 {\n &:is(.dark *) {\n &:hover {\n @media (hover: hover) {\n background-color: var(--input);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--input) 50%, transparent);\n }\n }\n }\n }\n }\n .dark\\:focus-visible\\:ring-destructive\\/40 {\n &:is(.dark *) {\n &:focus-visible {\n --tw-ring-color: var(--destructive);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent);\n }\n }\n }\n }\n .dark\\:aria-invalid\\:ring-destructive\\/40 {\n &:is(.dark *) {\n &[aria-invalid="true"] {\n --tw-ring-color: var(--destructive);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-ring-color: color-mix(in oklab, var(--destructive) 40%, transparent);\n }\n }\n }\n }\n .\\[\\&_svg\\]\\:pointer-events-none {\n & svg {\n pointer-events: none;\n }\n }\n .\\[\\&_svg\\]\\:shrink-0 {\n & svg {\n flex-shrink: 0;\n }\n }\n .\\[\\&_svg\\:not\\(\\[class\\*\\=\\\'size-\\\'\\]\\)\\]\\:size-4 {\n & svg:not([class*=\'size-\']) {\n width: calc(var(--spacing) * 4);\n height: calc(var(--spacing) * 4);\n }\n }\n .\\[\\.border-b\\]\\:pb-6 {\n &:is(.border-b) {\n padding-bottom: calc(var(--spacing) * 6);\n }\n }\n .\\[\\.border-t\\]\\:pt-6 {\n &:is(.border-t) {\n padding-top: calc(var(--spacing) * 6);\n }\n }\n}\n:root {\n --radius: 0.75rem;\n --background: oklch(0.985 0.005 280);\n --foreground: oklch(0.22 0.01 280);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.22 0.01 280);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.22 0.01 280);\n --primary: oklch(0.38 0.21 320);\n --primary-foreground: oklch(0.99 0 0);\n --secondary: oklch(0.96 0.01 280);\n --secondary-foreground: oklch(0.22 0.01 280);\n --muted: oklch(0.93 0.008 280);\n --muted-foreground: oklch(0.52 0.01 280);\n --accent: oklch(0.78 0.15 180);\n --accent-foreground: oklch(0.99 0 0);\n --destructive: oklch(0.55 0.24 27);\n --destructive-foreground: oklch(0.99 0 0);\n --border: oklch(0.9 0.005 280);\n --input: oklch(0.92 0.005 280);\n --ring: oklch(0.65 0.01 280);\n --sidebar: oklch(0.985 0.005 280);\n --sidebar-foreground: oklch(0.22 0.01 280);\n --sidebar-primary: oklch(0.38 0.21 320);\n --sidebar-primary-foreground: oklch(0.99 0 0);\n --sidebar-accent: oklch(0.96 0.01 280);\n --sidebar-accent-foreground: oklch(0.22 0.01 280);\n --sidebar-border: oklch(0.9 0.005 280);\n --sidebar-ring: oklch(0.65 0.01 280);\n}\n.dark {\n --background: oklch(0.22 0.01 280);\n --foreground: oklch(0.99 0 0);\n --card: oklch(0.27 0.015 320);\n --card-foreground: oklch(0.99 0 0);\n --popover: oklch(0.27 0.015 320);\n --popover-foreground: oklch(0.99 0 0);\n --primary: oklch(0.27 0.19 320);\n --primary-foreground: oklch(0.99 0 0);\n --secondary: oklch(0.3 0.01 280);\n --secondary-foreground: oklch(0.99 0 0);\n --muted: oklch(0.3 0.01 280);\n --muted-foreground: oklch(0.7 0.01 280);\n --accent: oklch(0.7 0.15 180);\n --accent-foreground: oklch(0.99 0 0);\n --destructive: oklch(0.55 0.24 27);\n --destructive-foreground: oklch(0.99 0 0);\n --border: oklch(0.3 0.01 280);\n --input: oklch(0.3 0.01 280);\n --ring: oklch(0.6 0.01 280);\n --sidebar: oklch(0.27 0.015 320);\n --sidebar-foreground: oklch(0.99 0 0);\n --sidebar-primary: oklch(0.55 0.21 320);\n --sidebar-primary-foreground: oklch(0.99 0 0);\n --sidebar-accent: oklch(0.3 0.01 280);\n --sidebar-accent-foreground: oklch(0.99 0 0);\n --sidebar-border: oklch(0.3 0.01 280);\n --sidebar-ring: oklch(0.6 0.01 280);\n}\n@layer base {\n * {\n border-color: var(--border);\n outline-color: var(--ring);\n @supports (color: color-mix(in lab, red, red)) {\n outline-color: color-mix(in oklab, var(--ring) 50%, transparent);\n }\n }\n body {\n background-color: var(--background);\n color: var(--foreground);\n }\n}\n@property --tw-scale-x {\n syntax: "*";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: "*";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: "*";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-rotate-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: "*";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-pan-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-pan-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-pinch-zoom {\n syntax: "*";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-space-x-reverse {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-divide-x-reverse {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-divide-y-reverse {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ordinal {\n syntax: "*";\n inherits: false;\n}\n@property --tw-slashed-zero {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-figure {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-spacing {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-fraction {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "<percentage>";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "<percentage>";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "<length>";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-outline-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-alpha {\n syntax: "<percentage>";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-drop-shadow-size {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: "*";\n inherits: false;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-pan-x: initial;\n --tw-pan-y: initial;\n --tw-pinch-zoom: initial;\n --tw-space-y-reverse: 0;\n --tw-space-x-reverse: 0;\n --tw-divide-x-reverse: 0;\n --tw-border-style: solid;\n --tw-divide-y-reverse: 0;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-ordinal: initial;\n --tw-slashed-zero: initial;\n --tw-numeric-figure: initial;\n --tw-numeric-spacing: initial;\n --tw-numeric-fraction: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-outline-style: solid;\n --tw-blur: initial;\n --tw-brightness: initial;\n --tw-contrast: initial;\n --tw-grayscale: initial;\n --tw-hue-rotate: initial;\n --tw-invert: initial;\n --tw-opacity: initial;\n --tw-saturate: initial;\n --tw-sepia: initial;\n --tw-drop-shadow: initial;\n --tw-drop-shadow-color: initial;\n --tw-drop-shadow-alpha: 100%;\n --tw-drop-shadow-size: initial;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n }\n }\n}\n',""]);const s=i},540:e=>{"use strict";e.exports=function(e){var n=document.createElement("style");return e.setAttributes(n,e.attributes),e.insert(n,e.options),n}},601:e=>{"use strict";e.exports=function(e){return e[1]}},659:e=>{"use strict";var n={};e.exports=function(e,r){var t=function(e){if(void 0===n[e]){var r=document.querySelector(e);if(window.HTMLIFrameElement&&r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(e){r=null}n[e]=r}return n[e]}(e);if(!t)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");t.appendChild(r)}},726:e=>{"use strict";e.exports=r},825:e=>{"use strict";e.exports=function(e){if("undefined"==typeof document)return{update:function(){},remove:function(){}};var n=e.insertStyleElement(e);return{update:function(r){!function(e,n,r){var t="";r.supports&&(t+="@supports (".concat(r.supports,") {")),r.media&&(t+="@media ".concat(r.media," {"));var a=void 0!==r.layer;a&&(t+="@layer".concat(r.layer.length>0?" ".concat(r.layer):""," {")),t+=r.css,a&&(t+="}"),r.media&&(t+="}"),r.supports&&(t+="}");var o=r.sourceMap;o&&"undefined"!=typeof btoa&&(t+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(o))))," */")),n.styleTagTransform(t,e,n.options)}(n,e,r)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)}}}},848:(e,n,r)=>{"use strict";e.exports=r(20)}},a={};function o(n){var r=a[n];if(void 0!==r)return r.exports;var t=a[n]={id:n,exports:{}};return e[n](t,t.exports,o),t.exports}o.y=n,o.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return o.d(n,{a:n}),n},o.d=(e,n)=>{for(var r in n)o.o(n,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},o.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.p="",o.nc=void 0;var i={};return(0,o(126).w)(1),(()=>{"use strict";o.r(i),o.d(i,{SustainabilityDashboardTemplate:()=>v,SustainabilityTabTemplate:()=>rn});var e=o(72),n=o.n(e),r=o(825),t=o.n(r),a=o(659),s=o.n(a),l=o(56),c=o.n(l),d=o(540),p=o.n(d),u=o(113),m=o.n(u),b=o(497),f={};f.styleTagTransform=m(),f.setAttributes=c(),f.insert=s().bind(null,"head"),f.domAPI=t(),f.insertStyleElement=p(),n()(b.A,f),b.A&&b.A.locals&&b.A.locals;var g=o(726);const v=({label:e})=>{const[n,r]=(0,g.useState)(e);return g.default.createElement("div",null,g.default.createElement("h1",null,n),g.default.createElement("p",null,"Coming soon: https://github.com/liamgold/xperience-community-sustainability/issues/4."))};function h(){return h=Object.assign?Object.assign.bind():function(e){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var t in r)({}).hasOwnProperty.call(r,t)&&(e[t]=r[t])}return e},h.apply(null,arguments)}function w(e){var n,r,t="";if("string"==typeof e||"number"==typeof e)t+=e;else if("object"==typeof e)if(Array.isArray(e)){var a=e.length;for(n=0;n<a;n++)e[n]&&(r=w(e[n]))&&(t&&(t+=" "),t+=r)}else for(r in e)e[r]&&(t&&(t+=" "),t+=r);return t}function y(){for(var e,n,r=0,t="",a=arguments.length;r<a;r++)(e=arguments[r])&&(n=w(e))&&(t&&(t+=" "),t+=n);return t}const x=e=>{const n=N(e),{conflictingClassGroups:r,conflictingClassGroupModifiers:t}=e;return{getClassGroupId:e=>{const r=e.split("-");return""===r[0]&&1!==r.length&&r.shift(),k(r,n)||E(e)},getConflictingClassGroupIds:(e,n)=>{const a=r[e]||[];return n&&t[e]?[...a,...t[e]]:a}}},k=(e,n)=>{if(0===e.length)return n.classGroupId;const r=e[0],t=n.nextPart.get(r),a=t?k(e.slice(1),t):void 0;if(a)return a;if(0===n.validators.length)return;const o=e.join("-");return n.validators.find((({validator:e})=>e(o)))?.classGroupId},z=/^\[(.+)\]$/,E=e=>{if(z.test(e)){const n=z.exec(e)[1],r=n?.substring(0,n.indexOf(":"));if(r)return"arbitrary.."+r}},N=e=>{const{theme:n,classGroups:r}=e,t={nextPart:new Map,validators:[]};for(const e in r)C(r[e],t,e,n);return t},C=(e,n,r,t)=>{e.forEach((e=>{if("string"!=typeof e){if("function"==typeof e)return S(e)?void C(e(t),n,r,t):void n.validators.push({validator:e,classGroupId:r});Object.entries(e).forEach((([e,a])=>{C(a,j(n,e),r,t)}))}else(""===e?n:j(n,e)).classGroupId=r}))},j=(e,n)=>{let r=e;return n.split("-").forEach((e=>{r.nextPart.has(e)||r.nextPart.set(e,{nextPart:new Map,validators:[]}),r=r.nextPart.get(e)})),r},S=e=>e.isThemeGetter,_=e=>{if(e<1)return{get:()=>{},set:()=>{}};let n=0,r=new Map,t=new Map;const a=(a,o)=>{r.set(a,o),n++,n>e&&(n=0,t=r,r=new Map)};return{get(e){let n=r.get(e);return void 0!==n?n:void 0!==(n=t.get(e))?(a(e,n),n):void 0},set(e,n){r.has(e)?r.set(e,n):a(e,n)}}},M=e=>{const{prefix:n,experimentalParseClassName:r}=e;let t=e=>{const n=[];let r,t=0,a=0,o=0;for(let i=0;i<e.length;i++){let s=e[i];if(0===t&&0===a){if(":"===s){n.push(e.slice(o,i)),o=i+1;continue}if("/"===s){r=i;continue}}"["===s?t++:"]"===s?t--:"("===s?a++:")"===s&&a--}const i=0===n.length?e:e.substring(o),s=O(i);return{modifiers:n,hasImportantModifier:s!==i,baseClassName:s,maybePostfixModifierPosition:r&&r>o?r-o:void 0}};if(n){const e=n+":",r=t;t=n=>n.startsWith(e)?r(n.substring(e.length)):{isExternal:!0,modifiers:[],hasImportantModifier:!1,baseClassName:n,maybePostfixModifierPosition:void 0}}if(r){const e=t;t=n=>r({className:n,parseClassName:e})}return t},O=e=>e.endsWith("!")?e.substring(0,e.length-1):e.startsWith("!")?e.substring(1):e,A=e=>{const n=Object.fromEntries(e.orderSensitiveModifiers.map((e=>[e,!0])));return e=>{if(e.length<=1)return e;const r=[];let t=[];return e.forEach((e=>{"["===e[0]||n[e]?(r.push(...t.sort(),e),t=[]):t.push(e)})),r.push(...t.sort()),r}},P=/\s+/;function I(){let e,n,r=0,t="";for(;r<arguments.length;)(e=arguments[r++])&&(n=R(e))&&(t&&(t+=" "),t+=n);return t}const R=e=>{if("string"==typeof e)return e;let n,r="";for(let t=0;t<e.length;t++)e[t]&&(n=R(e[t]))&&(r&&(r+=" "),r+=n);return r};function T(e,...n){let r,t,a,o=function(s){const l=n.reduce(((e,n)=>n(e)),e());return r=(e=>({cache:_(e.cacheSize),parseClassName:M(e),sortModifiers:A(e),...x(e)}))(l),t=r.cache.get,a=r.cache.set,o=i,i(s)};function i(e){const n=t(e);if(n)return n;const o=((e,n)=>{const{parseClassName:r,getClassGroupId:t,getConflictingClassGroupIds:a,sortModifiers:o}=n,i=[],s=e.trim().split(P);let l="";for(let e=s.length-1;e>=0;e-=1){const n=s[e],{isExternal:c,modifiers:d,hasImportantModifier:p,baseClassName:u,maybePostfixModifierPosition:m}=r(n);if(c){l=n+(l.length>0?" "+l:l);continue}let b=!!m,f=t(b?u.substring(0,m):u);if(!f){if(!b){l=n+(l.length>0?" "+l:l);continue}if(f=t(u),!f){l=n+(l.length>0?" "+l:l);continue}b=!1}const g=o(d).join(":"),v=p?g+"!":g,h=v+f;if(i.includes(h))continue;i.push(h);const w=a(f,b);for(let e=0;e<w.length;++e){const n=w[e];i.push(v+n)}l=n+(l.length>0?" "+l:l)}return l})(e,r);return a(e,o),o}return function(){return o(I.apply(null,arguments))}}const G=e=>{const n=n=>n[e]||[];return n.isThemeGetter=!0,n},L=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,$=/^\((?:(\w[\w-]*):)?(.+)\)$/i,F=/^\d+\/\d+$/,W=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,D=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,U=/^(rgba?|hsla?|hwb|(ok)?(lab|lch))\(.+\)$/,V=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,B=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,q=e=>F.test(e),K=e=>!!e&&!Number.isNaN(Number(e)),Z=e=>!!e&&Number.isInteger(Number(e)),H=e=>e.endsWith("%")&&K(e.slice(0,-1)),J=e=>W.test(e),Y=()=>!0,Q=e=>D.test(e)&&!U.test(e),X=()=>!1,ee=e=>V.test(e),ne=e=>B.test(e),re=e=>!ae(e)&&!de(e),te=e=>ve(e,xe,X),ae=e=>L.test(e),oe=e=>ve(e,ke,Q),ie=e=>ve(e,ze,K),se=e=>ve(e,we,X),le=e=>ve(e,ye,ne),ce=e=>ve(e,Ne,ee),de=e=>$.test(e),pe=e=>he(e,ke),ue=e=>he(e,Ee),me=e=>he(e,we),be=e=>he(e,xe),fe=e=>he(e,ye),ge=e=>he(e,Ne,!0),ve=(e,n,r)=>{const t=L.exec(e);return!!t&&(t[1]?n(t[1]):r(t[2]))},he=(e,n,r=!1)=>{const t=$.exec(e);return!!t&&(t[1]?n(t[1]):r)},we=e=>"position"===e||"percentage"===e,ye=e=>"image"===e||"url"===e,xe=e=>"length"===e||"size"===e||"bg-size"===e,ke=e=>"length"===e,ze=e=>"number"===e,Ee=e=>"family-name"===e,Ne=e=>"shadow"===e,Ce=T((Symbol.toStringTag,()=>{const e=G("color"),n=G("font"),r=G("text"),t=G("font-weight"),a=G("tracking"),o=G("leading"),i=G("breakpoint"),s=G("container"),l=G("spacing"),c=G("radius"),d=G("shadow"),p=G("inset-shadow"),u=G("text-shadow"),m=G("drop-shadow"),b=G("blur"),f=G("perspective"),g=G("aspect"),v=G("ease"),h=G("animate"),w=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom",de,ae],y=()=>[de,ae,l],x=()=>[q,"full","auto",...y()],k=()=>[Z,"none","subgrid",de,ae],z=()=>["auto",{span:["full",Z,de,ae]},Z,de,ae],E=()=>[Z,"auto",de,ae],N=()=>["auto","min","max","fr",de,ae],C=()=>["auto",...y()],j=()=>[q,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...y()],S=()=>[e,de,ae],_=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom",me,se,{position:[de,ae]}],M=()=>["auto","cover","contain",be,te,{size:[de,ae]}],O=()=>[H,pe,oe],A=()=>["","none","full",c,de,ae],P=()=>["",K,pe,oe],I=()=>[K,H,me,se],R=()=>["","none",b,de,ae],T=()=>["none",K,de,ae],L=()=>["none",K,de,ae],$=()=>[K,de,ae],F=()=>[q,"full",...y()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[J],breakpoint:[J],color:[Y],container:[J],"drop-shadow":[J],ease:["in","out","in-out"],font:[re],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[J],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[J],shadow:[J],spacing:["px",K],text:[J],"text-shadow":[J],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",q,ae,de,g]}],container:["container"],columns:[{columns:[K,ae,de,s]}],"break-after":[{"break-after":["auto","avoid","all","avoid-page","page","left","right","column"]}],"break-before":[{"break-before":["auto","avoid","all","avoid-page","page","left","right","column"]}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:w()}],overflow:[{overflow:["auto","hidden","clip","visible","scroll"]}],"overflow-x":[{"overflow-x":["auto","hidden","clip","visible","scroll"]}],"overflow-y":[{"overflow-y":["auto","hidden","clip","visible","scroll"]}],overscroll:[{overscroll:["auto","contain","none"]}],"overscroll-x":[{"overscroll-x":["auto","contain","none"]}],"overscroll-y":[{"overscroll-y":["auto","contain","none"]}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:x()}],"inset-x":[{"inset-x":x()}],"inset-y":[{"inset-y":x()}],start:[{start:x()}],end:[{end:x()}],top:[{top:x()}],right:[{right:x()}],bottom:[{bottom:x()}],left:[{left:x()}],visibility:["visible","invisible","collapse"],z:[{z:[Z,"auto",de,ae]}],basis:[{basis:[q,"full","auto",s,...y()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[K,q,"auto","initial","none",ae]}],grow:[{grow:["",K,de,ae]}],shrink:[{shrink:["",K,de,ae]}],order:[{order:[Z,"first","last","none",de,ae]}],"grid-cols":[{"grid-cols":k()}],"col-start-end":[{col:z()}],"col-start":[{"col-start":E()}],"col-end":[{"col-end":E()}],"grid-rows":[{"grid-rows":k()}],"row-start-end":[{row:z()}],"row-start":[{"row-start":E()}],"row-end":[{"row-end":E()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":N()}],"auto-rows":[{"auto-rows":N()}],gap:[{gap:y()}],"gap-x":[{"gap-x":y()}],"gap-y":[{"gap-y":y()}],"justify-content":[{justify:["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe","normal"]}],"justify-items":[{"justify-items":["start","end","center","stretch","center-safe","end-safe","normal"]}],"justify-self":[{"justify-self":["auto","start","end","center","stretch","center-safe","end-safe"]}],"align-content":[{content:["normal","start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"]}],"align-items":[{items:["start","end","center","stretch","center-safe","end-safe",{baseline:["","last"]}]}],"align-self":[{self:["auto","start","end","center","stretch","center-safe","end-safe",{baseline:["","last"]}]}],"place-content":[{"place-content":["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"]}],"place-items":[{"place-items":["start","end","center","stretch","center-safe","end-safe","baseline"]}],"place-self":[{"place-self":["auto","start","end","center","stretch","center-safe","end-safe"]}],p:[{p:y()}],px:[{px:y()}],py:[{py:y()}],ps:[{ps:y()}],pe:[{pe:y()}],pt:[{pt:y()}],pr:[{pr:y()}],pb:[{pb:y()}],pl:[{pl:y()}],m:[{m:C()}],mx:[{mx:C()}],my:[{my:C()}],ms:[{ms:C()}],me:[{me:C()}],mt:[{mt:C()}],mr:[{mr:C()}],mb:[{mb:C()}],ml:[{ml:C()}],"space-x":[{"space-x":y()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":y()}],"space-y-reverse":["space-y-reverse"],size:[{size:j()}],w:[{w:[s,"screen",...j()]}],"min-w":[{"min-w":[s,"screen","none",...j()]}],"max-w":[{"max-w":[s,"screen","none","prose",{screen:[i]},...j()]}],h:[{h:["screen",...j()]}],"min-h":[{"min-h":["screen","none",...j()]}],"max-h":[{"max-h":["screen",...j()]}],"font-size":[{text:["base",r,pe,oe]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[t,de,ie]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",H,ae]}],"font-family":[{font:[ue,ae,n]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[a,de,ae]}],"line-clamp":[{"line-clamp":[K,"none",de,ie]}],leading:[{leading:[o,...y()]}],"list-image":[{"list-image":["none",de,ae]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",de,ae]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:S()}],"text-color":[{text:S()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:["solid","dashed","dotted","double","wavy"]}],"text-decoration-thickness":[{decoration:[K,"from-font","auto",de,oe]}],"text-decoration-color":[{decoration:S()}],"underline-offset":[{"underline-offset":[K,"auto",de,ae]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:y()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",de,ae]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",de,ae]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:_()}],"bg-repeat":[{bg:["no-repeat",{repeat:["","x","y","space","round"]}]}],"bg-size":[{bg:M()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},Z,de,ae],radial:["",de,ae],conic:[Z,de,ae]},fe,le]}],"bg-color":[{bg:S()}],"gradient-from-pos":[{from:O()}],"gradient-via-pos":[{via:O()}],"gradient-to-pos":[{to:O()}],"gradient-from":[{from:S()}],"gradient-via":[{via:S()}],"gradient-to":[{to:S()}],rounded:[{rounded:A()}],"rounded-s":[{"rounded-s":A()}],"rounded-e":[{"rounded-e":A()}],"rounded-t":[{"rounded-t":A()}],"rounded-r":[{"rounded-r":A()}],"rounded-b":[{"rounded-b":A()}],"rounded-l":[{"rounded-l":A()}],"rounded-ss":[{"rounded-ss":A()}],"rounded-se":[{"rounded-se":A()}],"rounded-ee":[{"rounded-ee":A()}],"rounded-es":[{"rounded-es":A()}],"rounded-tl":[{"rounded-tl":A()}],"rounded-tr":[{"rounded-tr":A()}],"rounded-br":[{"rounded-br":A()}],"rounded-bl":[{"rounded-bl":A()}],"border-w":[{border:P()}],"border-w-x":[{"border-x":P()}],"border-w-y":[{"border-y":P()}],"border-w-s":[{"border-s":P()}],"border-w-e":[{"border-e":P()}],"border-w-t":[{"border-t":P()}],"border-w-r":[{"border-r":P()}],"border-w-b":[{"border-b":P()}],"border-w-l":[{"border-l":P()}],"divide-x":[{"divide-x":P()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":P()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:["solid","dashed","dotted","double","hidden","none"]}],"divide-style":[{divide:["solid","dashed","dotted","double","hidden","none"]}],"border-color":[{border:S()}],"border-color-x":[{"border-x":S()}],"border-color-y":[{"border-y":S()}],"border-color-s":[{"border-s":S()}],"border-color-e":[{"border-e":S()}],"border-color-t":[{"border-t":S()}],"border-color-r":[{"border-r":S()}],"border-color-b":[{"border-b":S()}],"border-color-l":[{"border-l":S()}],"divide-color":[{divide:S()}],"outline-style":[{outline:["solid","dashed","dotted","double","none","hidden"]}],"outline-offset":[{"outline-offset":[K,de,ae]}],"outline-w":[{outline:["",K,pe,oe]}],"outline-color":[{outline:S()}],shadow:[{shadow:["","none",d,ge,ce]}],"shadow-color":[{shadow:S()}],"inset-shadow":[{"inset-shadow":["none",p,ge,ce]}],"inset-shadow-color":[{"inset-shadow":S()}],"ring-w":[{ring:P()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:S()}],"ring-offset-w":[{"ring-offset":[K,oe]}],"ring-offset-color":[{"ring-offset":S()}],"inset-ring-w":[{"inset-ring":P()}],"inset-ring-color":[{"inset-ring":S()}],"text-shadow":[{"text-shadow":["none",u,ge,ce]}],"text-shadow-color":[{"text-shadow":S()}],opacity:[{opacity:[K,de,ae]}],"mix-blend":[{"mix-blend":["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity","plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"]}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[K]}],"mask-image-linear-from-pos":[{"mask-linear-from":I()}],"mask-image-linear-to-pos":[{"mask-linear-to":I()}],"mask-image-linear-from-color":[{"mask-linear-from":S()}],"mask-image-linear-to-color":[{"mask-linear-to":S()}],"mask-image-t-from-pos":[{"mask-t-from":I()}],"mask-image-t-to-pos":[{"mask-t-to":I()}],"mask-image-t-from-color":[{"mask-t-from":S()}],"mask-image-t-to-color":[{"mask-t-to":S()}],"mask-image-r-from-pos":[{"mask-r-from":I()}],"mask-image-r-to-pos":[{"mask-r-to":I()}],"mask-image-r-from-color":[{"mask-r-from":S()}],"mask-image-r-to-color":[{"mask-r-to":S()}],"mask-image-b-from-pos":[{"mask-b-from":I()}],"mask-image-b-to-pos":[{"mask-b-to":I()}],"mask-image-b-from-color":[{"mask-b-from":S()}],"mask-image-b-to-color":[{"mask-b-to":S()}],"mask-image-l-from-pos":[{"mask-l-from":I()}],"mask-image-l-to-pos":[{"mask-l-to":I()}],"mask-image-l-from-color":[{"mask-l-from":S()}],"mask-image-l-to-color":[{"mask-l-to":S()}],"mask-image-x-from-pos":[{"mask-x-from":I()}],"mask-image-x-to-pos":[{"mask-x-to":I()}],"mask-image-x-from-color":[{"mask-x-from":S()}],"mask-image-x-to-color":[{"mask-x-to":S()}],"mask-image-y-from-pos":[{"mask-y-from":I()}],"mask-image-y-to-pos":[{"mask-y-to":I()}],"mask-image-y-from-color":[{"mask-y-from":S()}],"mask-image-y-to-color":[{"mask-y-to":S()}],"mask-image-radial":[{"mask-radial":[de,ae]}],"mask-image-radial-from-pos":[{"mask-radial-from":I()}],"mask-image-radial-to-pos":[{"mask-radial-to":I()}],"mask-image-radial-from-color":[{"mask-radial-from":S()}],"mask-image-radial-to-color":[{"mask-radial-to":S()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"]}],"mask-image-conic-pos":[{"mask-conic":[K]}],"mask-image-conic-from-pos":[{"mask-conic-from":I()}],"mask-image-conic-to-pos":[{"mask-conic-to":I()}],"mask-image-conic-from-color":[{"mask-conic-from":S()}],"mask-image-conic-to-color":[{"mask-conic-to":S()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:_()}],"mask-repeat":[{mask:["no-repeat",{repeat:["","x","y","space","round"]}]}],"mask-size":[{mask:M()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",de,ae]}],filter:[{filter:["","none",de,ae]}],blur:[{blur:R()}],brightness:[{brightness:[K,de,ae]}],contrast:[{contrast:[K,de,ae]}],"drop-shadow":[{"drop-shadow":["","none",m,ge,ce]}],"drop-shadow-color":[{"drop-shadow":S()}],grayscale:[{grayscale:["",K,de,ae]}],"hue-rotate":[{"hue-rotate":[K,de,ae]}],invert:[{invert:["",K,de,ae]}],saturate:[{saturate:[K,de,ae]}],sepia:[{sepia:["",K,de,ae]}],"backdrop-filter":[{"backdrop-filter":["","none",de,ae]}],"backdrop-blur":[{"backdrop-blur":R()}],"backdrop-brightness":[{"backdrop-brightness":[K,de,ae]}],"backdrop-contrast":[{"backdrop-contrast":[K,de,ae]}],"backdrop-grayscale":[{"backdrop-grayscale":["",K,de,ae]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[K,de,ae]}],"backdrop-invert":[{"backdrop-invert":["",K,de,ae]}],"backdrop-opacity":[{"backdrop-opacity":[K,de,ae]}],"backdrop-saturate":[{"backdrop-saturate":[K,de,ae]}],"backdrop-sepia":[{"backdrop-sepia":["",K,de,ae]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":y()}],"border-spacing-x":[{"border-spacing-x":y()}],"border-spacing-y":[{"border-spacing-y":y()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",de,ae]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[K,"initial",de,ae]}],ease:[{ease:["linear","initial",v,de,ae]}],delay:[{delay:[K,de,ae]}],animate:[{animate:["none",h,de,ae]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[f,de,ae]}],"perspective-origin":[{"perspective-origin":w()}],rotate:[{rotate:T()}],"rotate-x":[{"rotate-x":T()}],"rotate-y":[{"rotate-y":T()}],"rotate-z":[{"rotate-z":T()}],scale:[{scale:L()}],"scale-x":[{"scale-x":L()}],"scale-y":[{"scale-y":L()}],"scale-z":[{"scale-z":L()}],"scale-3d":["scale-3d"],skew:[{skew:$()}],"skew-x":[{"skew-x":$()}],"skew-y":[{"skew-y":$()}],transform:[{transform:[de,ae,"","none","gpu","cpu"]}],"transform-origin":[{origin:w()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:F()}],"translate-x":[{"translate-x":F()}],"translate-y":[{"translate-y":F()}],"translate-z":[{"translate-z":F()}],"translate-none":["translate-none"],accent:[{accent:S()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:S()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",de,ae]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":y()}],"scroll-mx":[{"scroll-mx":y()}],"scroll-my":[{"scroll-my":y()}],"scroll-ms":[{"scroll-ms":y()}],"scroll-me":[{"scroll-me":y()}],"scroll-mt":[{"scroll-mt":y()}],"scroll-mr":[{"scroll-mr":y()}],"scroll-mb":[{"scroll-mb":y()}],"scroll-ml":[{"scroll-ml":y()}],"scroll-p":[{"scroll-p":y()}],"scroll-px":[{"scroll-px":y()}],"scroll-py":[{"scroll-py":y()}],"scroll-ps":[{"scroll-ps":y()}],"scroll-pe":[{"scroll-pe":y()}],"scroll-pt":[{"scroll-pt":y()}],"scroll-pr":[{"scroll-pr":y()}],"scroll-pb":[{"scroll-pb":y()}],"scroll-pl":[{"scroll-pl":y()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",de,ae]}],fill:[{fill:["none",...S()]}],"stroke-w":[{stroke:[K,pe,oe,ie]}],stroke:[{stroke:["none",...S()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}}));function je(...e){return Ce(y(e))}function Se({className:e,...n}){return g.createElement("div",h({"data-slot":"card",className:je("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",e)},n))}function _e({className:e,...n}){return g.createElement("div",h({"data-slot":"card-header",className:je("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",e)},n))}function Me({className:e,...n}){return g.createElement("div",h({"data-slot":"card-title",className:je("leading-none font-semibold",e)},n))}function Oe({className:e,...n}){return g.createElement("div",h({"data-slot":"card-description",className:je("text-muted-foreground text-sm",e)},n))}function Ae({className:e,...n}){return g.createElement("div",h({"data-slot":"card-content",className:je("px-6",e)},n))}function Pe(e,n){if("function"==typeof e)return e(n);null!=e&&(e.current=n)}var Ie=o(848);function Re(e){const n=Ge(e),r=g.forwardRef(((e,r)=>{const{children:t,...a}=e,o=g.Children.toArray(t),i=o.find($e);if(i){const e=i.props.children,t=o.map((n=>n===i?g.Children.count(e)>1?g.Children.only(null):g.isValidElement(e)?e.props.children:null:n));return(0,Ie.jsx)(n,{...a,ref:r,children:g.isValidElement(e)?g.cloneElement(e,void 0,t):null})}return(0,Ie.jsx)(n,{...a,ref:r,children:t})}));return r.displayName=`${e}.Slot`,r}var Te=Re("Slot");function Ge(e){const n=g.forwardRef(((e,n)=>{const{children:r,...t}=e,a=function(...e){return g.useCallback(function(...e){return n=>{let r=!1;const t=e.map((e=>{const t=Pe(e,n);return r||"function"!=typeof t||(r=!0),t}));if(r)return()=>{for(let n=0;n<t.length;n++){const r=t[n];"function"==typeof r?r():Pe(e[n],null)}}}}(...e),e)}(g.isValidElement(r)?function(e){let n=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,r=n&&"isReactWarning"in n&&n.isReactWarning;return r?e.ref:(n=Object.getOwnPropertyDescriptor(e,"ref")?.get,r=n&&"isReactWarning"in n&&n.isReactWarning,r?e.props.ref:e.props.ref||e.ref)}(r):void 0,n);if(g.isValidElement(r)){const e=function(e,n){const r={...n};for(const t in n){const a=e[t],o=n[t];/^on[A-Z]/.test(t)?a&&o?r[t]=(...e)=>{const n=o(...e);return a(...e),n}:a&&(r[t]=a):"style"===t?r[t]={...a,...o}:"className"===t&&(r[t]=[a,o].filter(Boolean).join(" "))}return{...e,...r}}(t,r.props);return r.type!==g.Fragment&&(e.ref=a),g.cloneElement(r,e)}return g.Children.count(r)>1?g.Children.only(null):null}));return n.displayName=`${e}.SlotClone`,n}var Le=Symbol("radix.slottable");function $e(e){return g.isValidElement(e)&&"function"==typeof e.type&&"__radixId"in e.type&&e.type.__radixId===Le}const Fe=e=>"boolean"==typeof e?`${e}`:0===e?"0":e,We=y,De=(Ue="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",Ve={variants:{variant:{default:"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",destructive:"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",outline:"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",secondary:"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-9 px-4 py-2 has-[>svg]:px-3",sm:"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",lg:"h-10 rounded-md px-6 has-[>svg]:px-4",icon:"size-9"}},defaultVariants:{variant:"default",size:"default"}},e=>{var n;if(null==(null==Ve?void 0:Ve.variants))return We(Ue,null==e?void 0:e.class,null==e?void 0:e.className);const{variants:r,defaultVariants:t}=Ve,a=Object.keys(r).map((n=>{const a=null==e?void 0:e[n],o=null==t?void 0:t[n];if(null===a)return null;const i=Fe(a)||Fe(o);return r[n][i]})),o=e&&Object.entries(e).reduce(((e,n)=>{let[r,t]=n;return void 0===t||(e[r]=t),e}),{}),i=null==Ve||null===(n=Ve.compoundVariants)||void 0===n?void 0:n.reduce(((e,n)=>{let{class:r,className:a,...i}=n;return Object.entries(i).every((e=>{let[n,r]=e;return Array.isArray(r)?r.includes({...t,...o}[n]):{...t,...o}[n]===r}))?[...e,r,a]:e}),[]);return We(Ue,a,i,null==e?void 0:e.class,null==e?void 0:e.className)});var Ue,Ve;function Be({className:e,variant:n,size:r,asChild:t=!1,...a}){const o=t?Te:"button";return g.createElement(o,h({"data-slot":"button",className:je(De({variant:n,size:r,className:e}))},a))}const qe=e=>{const n=(e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,((e,n,r)=>r?r.toUpperCase():n.toLowerCase())))(e);return n.charAt(0).toUpperCase()+n.slice(1)},Ke=(...e)=>e.filter(((e,n,r)=>Boolean(e)&&""!==e.trim()&&r.indexOf(e)===n)).join(" ").trim(),Ze=e=>{for(const n in e)if(n.startsWith("aria-")||"role"===n||"title"===n)return!0};var He={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};const Je=(0,g.forwardRef)((({color:e="currentColor",size:n=24,strokeWidth:r=2,absoluteStrokeWidth:t,className:a="",children:o,iconNode:i,...s},l)=>(0,g.createElement)("svg",{ref:l,...He,width:n,height:n,stroke:e,strokeWidth:t?24*Number(r)/Number(n):r,className:Ke("lucide",a),...!o&&!Ze(s)&&{"aria-hidden":"true"},...s},[...i.map((([e,n])=>(0,g.createElement)(e,n))),...Array.isArray(o)?o:[o]]))),Ye=((e,n)=>{const r=(0,g.forwardRef)((({className:r,...t},a)=>{return(0,g.createElement)(Je,{ref:a,iconNode:n,className:Ke(`lucide-${o=qe(e),o.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}`,`lucide-${e}`,r),...t});var o}));return r.displayName=qe(e),r})("loader-circle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);var Qe=o(90),Xe=function(e){return e[e.Available=0]="Available",e[e.NotAvailable=1]="NotAvailable",e}(Xe||{});const en={"A+":"Extremely efficient (≤ 0.040g CO₂ per page view)",A:"Very efficient (≤ 0.079g CO₂ per page view)",B:"Efficient (≤ 0.145g CO₂ per page view)",C:"Moderate efficiency (≤ 0.209g CO₂ per page view)",D:"Low efficiency (≤ 0.278g CO₂ per page view)",E:"Poor efficiency (≤ 0.359g CO₂ per page view)",F:"Very poor efficiency (> 0.359g CO₂ per page view)"},nn={"A+":"text-emerald-600",A:"text-green-600",B:"text-lime-600",C:"text-yellow-600",D:"text-amber-600",E:"text-orange-600",F:"text-red-600"},rn=e=>{const[n,r]=(0,g.useState)(!1),[t,a]=(0,g.useState)(null),[o,i]=(0,g.useState)(e?.sustainabilityData),{execute:s}=(0,Qe.usePageCommand)("RunReport",{after:e=>{i(e?.sustainabilityData),r(!1),a(null)},onError:e=>{r(!1),a("Failed to run sustainability report. Please try again."),console.error("Sustainability report error:",e)}});return null==o?g.default.createElement("div",{className:"p-4 space-y-4"},g.default.createElement("h1",{className:"text-2xl font-semibold tracking-tight text-foreground"},"Sustainability Report"),g.default.createElement("div",{className:"flex items-center justify-center min-h-[60vh]"},g.default.createElement(Se,{className:"w-full max-w-md text-center bg-card text-card-foreground"},g.default.createElement(_e,null,g.default.createElement(Me,{className:"text-xl"},"No Sustainability Data Found")),g.default.createElement(Ae,{className:"space-y-4"},e?.pageAvailability===Xe.Available?g.default.createElement(g.default.Fragment,null,g.default.createElement("p",{className:"text-muted-foreground"},"We haven't retrieved any sustainability data for this page yet."),t&&g.default.createElement("div",{className:"text-sm text-red-600 bg-red-50 border border-red-200 rounded p-3"},t),g.default.createElement(Be,{disabled:n,onClick:()=>{r(!0),a(null),s()}},n&&g.default.createElement(Ye,{className:"animate-spin"}),"Run Sustainability Report")):g.default.createElement(g.default.Fragment,null,g.default.createElement("p",{className:"text-muted-foreground"},"The page is not available, so we cannot retrieve the data.")))))):g.default.createElement("div",{className:"p-4 space-y-4"},g.default.createElement("h1",{className:"text-2xl font-semibold tracking-tight text-foreground"},"Sustainability Report"),g.default.createElement("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4"},g.default.createElement("div",{className:"md:col-span-2 space-y-4"},o.resourceGroups.map((e=>{return g.default.createElement(Se,{key:e.type},g.default.createElement(_e,null,g.default.createElement(Me,null,e.name),g.default.createElement(Oe,null,"Total size: ",e.totalSize.toFixed(2),"KB")),g.default.createElement(Ae,null,(n=e.resources).length>0?g.default.createElement("ul",{className:"list-disc list-inside text-sm space-y-0.5"},n.map(((e,n)=>g.default.createElement("li",{key:n},e.url," (",e.size.toFixed(2),"KB)")))):g.default.createElement("p",{className:"text-muted-foreground text-sm"},"No resources found.")));var n}))),g.default.createElement("div",{className:"space-y-4"},g.default.createElement(Se,{className:"max-w-sm w-full"},g.default.createElement(_e,null,g.default.createElement(Me,null,"Sustainability report"),g.default.createElement(Oe,null,"Last tested: ",o.lastRunDate)),g.default.createElement(Ae,{className:"space-y-3"},t&&g.default.createElement("div",{className:"text-sm text-red-600 bg-red-50 border border-red-200 rounded p-3"},t),g.default.createElement(Be,{disabled:n,onClick:()=>{r(!0),a(null),s()},className:"bg-primary text-primary-foreground hover:bg-primary/90 w-full"},n&&g.default.createElement(Ye,{className:"animate-spin"}),"Run again"))),g.default.createElement(Se,{className:"max-w-sm w-full"},g.default.createElement(_e,null,g.default.createElement(Me,null,"Page size")),g.default.createElement(Ae,{className:"text-xl font-semibold"},o.totalSize.toFixed(2),"KB")),g.default.createElement(Se,{className:"max-w-sm w-full"},g.default.createElement(_e,null,g.default.createElement(Me,null,"CO₂ per page view")),g.default.createElement(Ae,{className:"text-xl font-semibold"},o.totalEmissions.toFixed(4),"g")),g.default.createElement(Se,{className:"max-w-sm w-full"},g.default.createElement(_e,null,g.default.createElement(Me,null,"Carbon rating"),g.default.createElement(Oe,null,en[o.carbonRating]||"No rating available")),g.default.createElement(Ae,{className:je("text-3xl font-bold",nn[o.carbonRating]||"text-muted-foreground")},o.carbonRating)))))}})(),i})())}}})); \ No newline at end of file diff --git a/src/Client/dist/entry.kxh.312f02820374d69a4ac8.js.LICENSE.txt b/src/Client/dist/entry.kxh.312f02820374d69a4ac8.js.LICENSE.txt deleted file mode 100644 index 572f865..0000000 --- a/src/Client/dist/entry.kxh.312f02820374d69a4ac8.js.LICENSE.txt +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license React - * react-jsx-runtime.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * @license lucide-react v0.509.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */ diff --git a/src/Client/lib/utils.ts b/src/Client/lib/utils.ts deleted file mode 100644 index a5ef193..0000000 --- a/src/Client/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/src/Client/package-lock.json b/src/Client/package-lock.json index 3577e04..16eb769 100644 --- a/src/Client/package-lock.json +++ b/src/Client/package-lock.json @@ -10,18 +10,11 @@ "dependencies": { "@kentico/xperience-admin-base": "30.4.2", "@kentico/xperience-admin-components": "30.4.2", - "@radix-ui/react-slot": "^1.2.2", - "@tailwindcss/postcss": "^4.1.6", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "css-loader": "^7.1.2", - "lucide-react": "^0.509.0", "postcss-loader": "^8.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "style-loader": "^4.0.0", - "tailwind-merge": "^3.2.0", - "tw-animate-css": "^1.2.9" + "style-loader": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.26.10", @@ -34,24 +27,11 @@ "@types/react-dom": "^18.3.6", "babel-loader": "^10.0.0", "postcss": "^8.5.3", - "tailwindcss": "^4.1.6", "webpack": "5.99.5", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@amcharts/amcharts5": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/@amcharts/amcharts5/-/amcharts5-5.12.1.tgz", @@ -89,6 +69,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2263,18 +2244,6 @@ "@swc/helpers": "^0.5.0" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -2560,39 +2529,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@react-aria/focus": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", @@ -2998,267 +2934,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@tailwindcss/node": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", - "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.29.2", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.6" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", - "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-x64": "4.1.6", - "@tailwindcss/oxide-freebsd-x64": "4.1.6", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-x64-musl": "4.1.6", - "@tailwindcss/oxide-wasm32-wasi": "4.1.6", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", - "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", - "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", - "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", - "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", - "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", - "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", - "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", - "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", - "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", - "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", - "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", - "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.6.tgz", - "integrity": "sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.6", - "@tailwindcss/oxide": "4.1.6", - "postcss": "^8.4.41", - "tailwindcss": "4.1.6" - } - }, "node_modules/@tippyjs/react": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", @@ -4572,15 +4247,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -4590,18 +4256,6 @@ "node": ">=6.0" } }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -5587,15 +5241,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -6808,15 +6453,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/jpeg-exif": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", @@ -6905,234 +6541,6 @@ "shell-quote": "^1.8.1" } }, - "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7193,24 +6601,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "0.509.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.509.0.tgz", - "integrity": "sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, "node_modules/markerjs2": { "version": "2.32.4", "resolved": "https://registry.npmjs.org/markerjs2/-/markerjs2-2.32.4.tgz", @@ -7343,42 +6733,6 @@ "dev": true, "license": "ISC" }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9240,22 +8594,6 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, - "node_modules/tailwind-merge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", - "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", - "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", - "license": "MIT" - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -9265,32 +8603,6 @@ "node": ">=6" } }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/terser": { "version": "5.39.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", @@ -9442,15 +8754,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tw-animate-css": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.9.tgz", - "integrity": "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Wombosvideo" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/src/Client/package.json b/src/Client/package.json index b8ffedd..1d35397 100644 --- a/src/Client/package.json +++ b/src/Client/package.json @@ -11,18 +11,11 @@ "dependencies": { "@kentico/xperience-admin-base": "30.4.2", "@kentico/xperience-admin-components": "30.4.2", - "@radix-ui/react-slot": "^1.2.2", - "@tailwindcss/postcss": "^4.1.6", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "css-loader": "^7.1.2", - "lucide-react": "^0.509.0", "postcss-loader": "^8.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "style-loader": "^4.0.0", - "tailwind-merge": "^3.2.0", - "tw-animate-css": "^1.2.9" + "style-loader": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.26.10", @@ -35,7 +28,6 @@ "@types/react-dom": "^18.3.6", "babel-loader": "^10.0.0", "postcss": "^8.5.3", - "tailwindcss": "^4.1.6", "webpack": "5.99.5", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2" diff --git a/src/Client/postcss.config.mjs b/src/Client/postcss.config.mjs deleted file mode 100644 index c2ddf74..0000000 --- a/src/Client/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; diff --git a/src/Client/src/Sustainability/SustainabilityTabTemplate.tsx b/src/Client/src/Sustainability/SustainabilityTabTemplate.tsx index c6b4d00..0de0049 100644 --- a/src/Client/src/Sustainability/SustainabilityTabTemplate.tsx +++ b/src/Client/src/Sustainability/SustainabilityTabTemplate.tsx @@ -1,14 +1,17 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Loader2 } from "lucide-react"; -import { cn } from "@/lib/utils"; + Button, + ButtonColor, + ButtonSize, + Stack, + Headline, + HeadlineSize, + Row, + Column, + Spacing, + Divider, +} from "@kentico/xperience-admin-components"; import { usePageCommand } from "@kentico/xperience-admin-base"; enum PageAvailabilityStatus { @@ -39,29 +42,225 @@ type ExternalResourceGroup = { }; const ratingDescriptions: Record<string, string> = { - "A+": "Extremely efficient (≤ 0.040g CO₂ per page view)", - A: "Very efficient (≤ 0.079g CO₂ per page view)", - B: "Efficient (≤ 0.145g CO₂ per page view)", - C: "Moderate efficiency (≤ 0.209g CO₂ per page view)", - D: "Low efficiency (≤ 0.278g CO₂ per page view)", - E: "Poor efficiency (≤ 0.359g CO₂ per page view)", - F: "Very poor efficiency (> 0.359g CO₂ per page view)", + "A+": "Extremely efficient", + A: "Very efficient", + B: "Efficient", + C: "Moderate efficiency", + D: "Low efficiency", + E: "Poor efficiency", + F: "Very poor efficiency", }; -const ratingColor: Record<string, string> = { - "A+": "text-emerald-600", // 🌿 Ultra efficient - A: "text-green-600", // ✅ Very efficient - B: "text-lime-600", // 👍 Efficient - C: "text-yellow-600", // 😐 Moderate - D: "text-amber-600", // ⚠️ Low - E: "text-orange-600", // 🚨 Poor - F: "text-red-600", // ❌ Very poor +const ratingColors: Record<string, { primary: string; bg: string; border: string }> = { + "A+": { primary: "#059669", bg: "#d1fae5", border: "#6ee7b7" }, + A: { primary: "#16a34a", bg: "#dcfce7", border: "#86efac" }, + B: { primary: "#65a30d", bg: "#ecfccb", border: "#bef264" }, + C: { primary: "#ca8a04", bg: "#fef9c3", border: "#fde047" }, + D: { primary: "#ea580c", bg: "#ffedd5", border: "#fdba74" }, + E: { primary: "#dc2626", bg: "#fee2e2", border: "#fca5a5" }, + F: { primary: "#b91c1c", bg: "#fee2e2", border: "#f87171" }, }; const Commands = { RunReport: "RunReport", }; +const StatCard = ({ + label, + value, + subtitle, +}: { + label: string; + value: string; + subtitle?: string; +}) => ( + <div + style={{ + padding: "20px", + background: "white", + border: "1px solid #e5e7eb", + borderRadius: "8px", + boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + }} + > + <div + style={{ + fontSize: "13px", + fontWeight: 600, + color: "#6b7280", + textTransform: "uppercase", + letterSpacing: "0.5px", + marginBottom: "8px", + }} + > + {label} + </div> + <div + style={{ + fontSize: "28px", + fontWeight: 700, + color: "#111827", + marginBottom: subtitle ? "4px" : "0", + }} + > + {value} + </div> + {subtitle && ( + <div style={{ fontSize: "12px", color: "#9ca3af" }}>{subtitle}</div> + )} + </div> +); + +// Style constants for ResourceGroupCard +const resourceGroupCardStyles = { + container: { + background: "white", + border: "1px solid #e5e7eb", + borderRadius: "8px", + overflow: "hidden" as const, + }, + header: { + padding: "16px 20px", + background: "#f9fafb", + borderBottom: "1px solid #e5e7eb", + display: "flex", + justifyContent: "space-between" as const, + alignItems: "center" as const, + }, + title: { + fontSize: "15px", + fontWeight: 600, + color: "#111827", + }, + subtitle: { + fontSize: "13px", + color: "#6b7280", + marginTop: "2px", + }, + badge: { + padding: "4px 12px", + background: "#eff6ff", + color: "#1e40af", + fontSize: "13px", + fontWeight: 600, + borderRadius: "12px", + }, + listContainer: { + padding: "12px 20px", + }, + resourceItem: (isLast: boolean) => ({ + padding: "12px 0", + borderBottom: isLast ? "none" : "1px solid #f3f4f6", + }), + resourceRow: { + display: "flex", + justifyContent: "space-between" as const, + alignItems: "flex-start" as const, + gap: "12px", + }, + resourceInfo: { + flex: 1, + minWidth: 0, + }, + fileName: { + fontSize: "13px", + fontWeight: 500, + color: "#111827", + marginBottom: "2px", + wordBreak: "break-word" as const, + }, + filePath: { + fontSize: "12px", + color: "#9ca3af", + overflow: "hidden", + textOverflow: "ellipsis" as const, + whiteSpace: "nowrap" as const, + }, + fileSize: { + fontSize: "13px", + fontWeight: 600, + color: "#6b7280", + whiteSpace: "nowrap" as const, + }, + expandButtonContainer: { + marginTop: "12px", + width: "100%", + display: "flex" as const, + justifyContent: "center" as const, + }, +}; + +const ResourceGroupCard = ({ + group, + totalPageSize +}: { + group: ExternalResourceGroup; + totalPageSize: number; +}) => { + const [expanded, setExpanded] = useState(false); + const displayCount = expanded ? group.resources.length : 3; + + return ( + <div style={resourceGroupCardStyles.container}> + <div style={resourceGroupCardStyles.header}> + <div> + <div style={resourceGroupCardStyles.title}> + {group.name} + </div> + <div style={resourceGroupCardStyles.subtitle}> + {group.resources.length} resource + {group.resources.length !== 1 ? "s" : ""} •{" "} + {group.totalSize.toFixed(2)} KB + </div> + </div> + <div style={resourceGroupCardStyles.badge}> + {((group.totalSize / totalPageSize) * 100).toFixed(1)}% of page + </div> + </div> + {group.resources.length > 0 && ( + <div style={resourceGroupCardStyles.listContainer}> + {group.resources.slice(0, displayCount).map((resource, idx) => { + const fileName = resource.url.split("/").pop() || resource.url; + const path = resource.url.substring( + 0, + resource.url.lastIndexOf("/") + 1 + ); + const isLast = idx === displayCount - 1; + + return ( + <div key={idx} style={resourceGroupCardStyles.resourceItem(isLast)}> + <div style={resourceGroupCardStyles.resourceRow}> + <div style={resourceGroupCardStyles.resourceInfo}> + <div style={resourceGroupCardStyles.fileName}> + {fileName} + </div> + <div style={resourceGroupCardStyles.filePath} title={path}> + {path} + </div> + </div> + <div style={resourceGroupCardStyles.fileSize}> + {resource.size.toFixed(2)} KB + </div> + </div> + </div> + ); + })} + {group.resources.length > 3 && ( + <div style={resourceGroupCardStyles.expandButtonContainer}> + <Button + label={expanded ? "Show less" : `Show ${group.resources.length - 3} more`} + onClick={() => setExpanded(!expanded)} + color={ButtonColor.Secondary} + size={ButtonSize.S} + /> + </div> + )} + </div> + )} + </div> + ); +}; + export const SustainabilityTabTemplate = ( props: SustainabilityTabTemplateProps | null ) => { @@ -89,156 +288,305 @@ export const SustainabilityTabTemplate = ( if (data === undefined || data === null) { return ( - <div className="p-4 space-y-4"> - <h1 className="text-2xl font-semibold tracking-tight text-foreground"> - Sustainability Report - </h1> - - <div className="flex items-center justify-center min-h-[60vh]"> - <Card className="w-full max-w-md text-center bg-card text-card-foreground"> - <CardHeader> - <CardTitle className="text-xl"> - No Sustainability Data Found - </CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - {props?.pageAvailability === PageAvailabilityStatus.Available ? ( - <> - <p className="text-muted-foreground"> - We haven't retrieved any sustainability data for this page - yet. - </p> - {error && ( - <div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded p-3"> - {error} - </div> - )} + <div style={{ padding: "32px", maxWidth: "1400px", margin: "0 auto" }}> + <Stack spacing={Spacing.XL}> + <div + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }} + > + <Headline size={HeadlineSize.L}>Sustainability Report</Headline> + </div> + + <div + style={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "400px", + }} + > + <Card headline="No Data Available" fullHeight={false}> + <Stack spacing={Spacing.L}> + <p style={{ fontSize: "14px", color: "#6b7280" }}> + {props?.pageAvailability === PageAvailabilityStatus.Available + ? "Run a sustainability analysis to see your page's environmental impact." + : "This page is not available for analysis."} + </p> + {error && ( + <div + style={{ + padding: "12px 16px", + background: "#fef2f2", + border: "1px solid #fecaca", + borderRadius: "6px", + fontSize: "13px", + color: "#dc2626", + }} + > + {error} + </div> + )} + {props?.pageAvailability === + PageAvailabilityStatus.Available && ( <Button + label="Run Analysis" + color={ButtonColor.Primary} + size={ButtonSize.L} disabled={isLoading} + inProgress={isLoading} onClick={() => { setIsLoading(true); setError(null); submit(); }} - > - {isLoading && <Loader2 className="animate-spin" />} - Run Sustainability Report - </Button> - </> - ) : ( - <> - <p className="text-muted-foreground"> - The page is not available, so we cannot retrieve the data. - </p> - </> - )} - </CardContent> - </Card> - </div> + /> + )} + </Stack> + </Card> + </div> + </Stack> </div> ); } - const renderResourceList = (resources: ExternalResource[]) => - resources.length > 0 ? ( - <ul className="list-disc list-inside text-sm space-y-0.5"> - {resources.map((item, i) => ( - <li key={i}> - {item.url} ({item.size.toFixed(2)}KB) - </li> - ))} - </ul> - ) : ( - <p className="text-muted-foreground text-sm">No resources found.</p> - ); + const ratingColor = ratingColors[data.carbonRating] || ratingColors.C; + const totalResources = data.resourceGroups.reduce( + (sum, group) => sum + group.resources.length, + 0 + ); return ( - <div className="p-4 space-y-4"> - {/* New Title */} - <h1 className="text-2xl font-semibold tracking-tight text-foreground"> - Sustainability Report - </h1> - <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> - {/* Left column: Resource groups */} - <div className="md:col-span-2 space-y-4"> - {data.resourceGroups.map((group) => ( - <Card key={group.type}> - <CardHeader> - <CardTitle>{group.name}</CardTitle> - <CardDescription> - Total size: {group.totalSize.toFixed(2)}KB - </CardDescription> - </CardHeader> - <CardContent>{renderResourceList(group.resources)}</CardContent> - </Card> - ))} + <div style={{ padding: "32px", maxWidth: "1400px", margin: "0 auto" }}> + <Stack spacing={Spacing.XL}> + {/* Header */} + <div + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + flexWrap: "wrap", + gap: "16px", + }} + > + <div> + <Headline size={HeadlineSize.L}>Sustainability Report</Headline> + <div + style={{ + fontSize: "14px", + color: "#6b7280", + marginTop: "4px", + }} + > + Last analyzed: {data.lastRunDate} + </div> + </div> + <Button + label="Run New Analysis" + color={ButtonColor.Primary} + size={ButtonSize.M} + disabled={isLoading} + inProgress={isLoading} + onClick={() => { + setIsLoading(true); + setError(null); + submit(); + }} + /> </div> - {/* Right column: Summary */} - <div className="space-y-4"> - <Card className="max-w-sm w-full"> - <CardHeader> - <CardTitle>Sustainability report</CardTitle> - <CardDescription>Last tested: {data.lastRunDate}</CardDescription> - </CardHeader> - <CardContent className="space-y-3"> - {error && ( - <div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded p-3"> - {error} + {error && ( + <div + style={{ + padding: "16px", + background: "#fef2f2", + border: "1px solid #fecaca", + borderRadius: "8px", + fontSize: "14px", + color: "#dc2626", + }} + > + {error} + </div> + )} + + {/* Hero Carbon Rating Card */} + <div + style={{ + background: `linear-gradient(135deg, ${ratingColor.bg} 0%, white 100%)`, + border: `2px solid ${ratingColor.border}`, + borderRadius: "12px", + padding: "40px", + position: "relative", + overflow: "hidden", + }} + > + <div + style={{ + position: "absolute", + top: "-50px", + right: "-50px", + width: "200px", + height: "200px", + background: ratingColor.bg, + borderRadius: "50%", + opacity: 0.3, + }} + /> + <Row spacing={Spacing.XL}> + <Column colsLg={6} colsMd={12}> + <div style={{ position: "relative", zIndex: 1 }}> + <div + style={{ + fontSize: "14px", + fontWeight: 600, + color: ratingColor.primary, + textTransform: "uppercase", + letterSpacing: "1px", + marginBottom: "12px", + }} + > + Carbon Rating </div> - )} - <Button - disabled={isLoading} - onClick={() => { - setIsLoading(true); - setError(null); - submit(); - }} - className="bg-primary text-primary-foreground hover:bg-primary/90 w-full" - > - {isLoading && <Loader2 className="animate-spin" />} - Run again - </Button> - </CardContent> - </Card> - - <Card className="max-w-sm w-full"> - <CardHeader> - <CardTitle>Page size</CardTitle> - </CardHeader> - <CardContent className="text-xl font-semibold"> - {data.totalSize.toFixed(2)}KB - </CardContent> - </Card> - - <Card className="max-w-sm w-full"> - <CardHeader> - <CardTitle>CO₂ per page view</CardTitle> - </CardHeader> - <CardContent className="text-xl font-semibold"> - {data.totalEmissions.toFixed(4)}g - </CardContent> - </Card> - - {/* Carbon rating */} - <Card className="max-w-sm w-full"> - <CardHeader> - <CardTitle>Carbon rating</CardTitle> - <CardDescription> - {ratingDescriptions[data.carbonRating] || "No rating available"} - </CardDescription> - </CardHeader> - <CardContent - className={cn( - "text-3xl font-bold", - ratingColor[data.carbonRating] || "text-muted-foreground" - )} - > - {data.carbonRating} - </CardContent> - </Card> + <div + style={{ + fontSize: "120px", + fontWeight: 900, + color: ratingColor.primary, + lineHeight: 1, + marginBottom: "16px", + textShadow: `0 2px 8px ${ratingColor.bg}`, + }} + > + {data.carbonRating} + </div> + <div + style={{ + fontSize: "18px", + fontWeight: 600, + color: "#111827", + marginBottom: "8px", + }} + > + {ratingDescriptions[data.carbonRating]} + </div> + <div style={{ fontSize: "14px", color: "#6b7280" }}> + {data.carbonRating === "A+" || data.carbonRating === "A" + ? "This page has excellent carbon efficiency." + : data.carbonRating === "B" || data.carbonRating === "C" + ? "This page has room for improvement." + : "This page needs significant optimization."} + </div> + </div> + </Column> + <Column colsLg={6} colsMd={12}> + <Row spacing={Spacing.L} spacingY={Spacing.L}> + <Column colsLg={6} colsMd={6}> + <StatCard + label="CO₂ Emissions" + value={`${data.totalEmissions.toFixed(3)}g`} + subtitle="per page view" + /> + </Column> + <Column colsLg={6} colsMd={6}> + <StatCard + label="Page Weight" + value={`${(data.totalSize / 1024).toFixed(2)}MB`} + subtitle={`${data.totalSize.toFixed(0)} KB total`} + /> + </Column> + <Column colsLg={6} colsMd={6}> + <StatCard + label="Resources" + value={`${totalResources}`} + subtitle={`${data.resourceGroups.length} categories`} + /> + </Column> + <Column colsLg={6} colsMd={6}> + <StatCard + label="Efficiency" + value={ + data.totalEmissions < 0.1 + ? "Excellent" + : data.totalEmissions < 0.2 + ? "Good" + : data.totalEmissions < 0.3 + ? "Fair" + : "Poor" + } + subtitle="Overall rating" + /> + </Column> + </Row> + </Column> + </Row> </div> - </div> + + {/* Resource Breakdown */} + <div> + <Headline size={HeadlineSize.M} spacingBottom={Spacing.L}> + Resource Breakdown + </Headline> + <Stack spacing={Spacing.L}> + {data.resourceGroups + .sort((a, b) => b.totalSize - a.totalSize) + .map((group) => ( + <ResourceGroupCard key={group.type} group={group} totalPageSize={data.totalSize} /> + ))} + </Stack> + </div> + + {/* Tips Section */} + <div + style={{ + padding: "24px", + background: "#f0f9ff", + border: "1px solid #bae6fd", + borderRadius: "8px", + }} + > + <div + style={{ + fontSize: "15px", + fontWeight: 600, + color: "#0c4a6e", + marginBottom: "12px", + }} + > + 💡 Tips to improve your carbon footprint + </div> + <ul style={{ margin: 0, paddingLeft: "20px", color: "#075985" }}> + <li style={{ marginBottom: "6px" }}> + <strong>Use Image Variants</strong> - Configure responsive image variants + with specific dimensions and aspect ratios to serve optimized versions + for different contexts (hero banners, thumbnails, social media) + </li> + <li style={{ marginBottom: "6px" }}> + <strong>Enable AIRA's Smart Optimization</strong> - Leverage AIRA's AI-powered + features for automatic image format conversion, smart focal point detection, + and automated quality optimization during uploads + </li> + <li style={{ marginBottom: "6px" }}> + <strong>Automate Image Metadata</strong> - Use AIRA to automatically generate + alt texts, descriptions, and tags for better SEO while reducing manual work + </li> + <li style={{ marginBottom: "6px" }}> + <strong>Minimize CSS & JavaScript</strong> - Bundle and minify your assets to + reduce file sizes and decrease the number of HTTP requests + </li> + <li style={{ marginBottom: "6px" }}> + <strong>Enable Browser Caching</strong> - Configure cache headers for static + resources to reduce repeat downloads and server load + </li> + <li> + <strong>Implement Lazy Loading</strong> - Load images and resources only when + they're needed, improving initial page load performance + </li> + </ul> + </div> + </Stack> </div> ); }; diff --git a/src/Client/src/entry.tsx b/src/Client/src/entry.tsx index f3b4ea0..15b6472 100644 --- a/src/Client/src/entry.tsx +++ b/src/Client/src/entry.tsx @@ -1,5 +1,3 @@ -import "./sustainability.css"; - // Exposes components from the module. All added components need to be exported. export * from "./Sustainability/SustainabilityDashboardTemplate"; export * from "./Sustainability/SustainabilityTabTemplate"; diff --git a/src/Client/src/sustainability.css b/src/Client/src/sustainability.css deleted file mode 100644 index c9e0227..0000000 --- a/src/Client/src/sustainability.css +++ /dev/null @@ -1,128 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --radius: 0.75rem; - - --background: oklch(0.985 0.005 280); - --foreground: oklch(0.22 0.01 280); - - --card: oklch(1 0 0); - --card-foreground: oklch(0.22 0.01 280); - - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.22 0.01 280); - - --primary: oklch(0.38 0.21 320); /* XbyK Purple (#5d0088) */ - --primary-foreground: oklch(0.99 0 0); - - --secondary: oklch(0.96 0.01 280); /* Light gray */ - --secondary-foreground: oklch(0.22 0.01 280); - - --muted: oklch(0.93 0.008 280); - --muted-foreground: oklch(0.52 0.01 280); - - --accent: oklch(0.78 0.15 180); /* Teal */ - --accent-foreground: oklch(0.99 0 0); - - --destructive: oklch(0.55 0.24 27); - --destructive-foreground: oklch(0.99 0 0); - - --border: oklch(0.9 0.005 280); - --input: oklch(0.92 0.005 280); - --ring: oklch(0.65 0.01 280); - - --sidebar: oklch(0.985 0.005 280); - --sidebar-foreground: oklch(0.22 0.01 280); - --sidebar-primary: oklch(0.38 0.21 320); /* purple */ - --sidebar-primary-foreground: oklch(0.99 0 0); - --sidebar-accent: oklch(0.96 0.01 280); - --sidebar-accent-foreground: oklch(0.22 0.01 280); - --sidebar-border: oklch(0.9 0.005 280); - --sidebar-ring: oklch(0.65 0.01 280); -} - -.dark { - --background: oklch(0.22 0.01 280); - --foreground: oklch(0.99 0 0); - - --card: oklch(0.27 0.015 320); - --card-foreground: oklch(0.99 0 0); - - --popover: oklch(0.27 0.015 320); - --popover-foreground: oklch(0.99 0 0); - - --primary: oklch(0.27 0.19 320); /* Hover purple (#40005f) */ - --primary-foreground: oklch(0.99 0 0); - - --secondary: oklch(0.3 0.01 280); - --secondary-foreground: oklch(0.99 0 0); - - --muted: oklch(0.3 0.01 280); - --muted-foreground: oklch(0.7 0.01 280); - - --accent: oklch(0.7 0.15 180); /* Teal accent */ - --accent-foreground: oklch(0.99 0 0); - - --destructive: oklch(0.55 0.24 27); - --destructive-foreground: oklch(0.99 0 0); - - --border: oklch(0.3 0.01 280); - --input: oklch(0.3 0.01 280); - --ring: oklch(0.6 0.01 280); - - --sidebar: oklch(0.27 0.015 320); - --sidebar-foreground: oklch(0.99 0 0); - --sidebar-primary: oklch(0.55 0.21 320); - --sidebar-primary-foreground: oklch(0.99 0 0); - --sidebar-accent: oklch(0.3 0.01 280); - --sidebar-accent-foreground: oklch(0.99 0 0); - --sidebar-border: oklch(0.3 0.01 280); - --sidebar-ring: oklch(0.6 0.01 280); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - - body { - @apply bg-background text-foreground; - } -} diff --git a/src/Client/tsconfig.json b/src/Client/tsconfig.json index 750c71c..a5f5473 100644 --- a/src/Client/tsconfig.json +++ b/src/Client/tsconfig.json @@ -1,10 +1,6 @@ // Provides recommended TypeScript configuration { "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./*"] - }, "target": "es6", "module": "esnext", "moduleResolution": "node", diff --git a/src/XperienceCommunity.Sustainability.csproj b/src/XperienceCommunity.Sustainability.csproj index 89c4add..f1830f0 100644 --- a/src/XperienceCommunity.Sustainability.csproj +++ b/src/XperienceCommunity.Sustainability.csproj @@ -11,7 +11,7 @@ <PropertyGroup> <Title>Xperience by Kentico Sustainability XperienceCommunity.Sustainability - 2.1.0 + 2.2.0 Liam Goldfinch Liam Goldfinch icon.png diff --git a/src/images/Sustainability Report - Page Tab.png b/src/images/Sustainability Report - Page Tab.png deleted file mode 100644 index 0e00b48..0000000 Binary files a/src/images/Sustainability Report - Page Tab.png and /dev/null differ diff --git a/src/images/SustainabilityReport-PageTab.jpeg b/src/images/SustainabilityReport-PageTab.jpeg new file mode 100644 index 0000000..29d068b Binary files /dev/null and b/src/images/SustainabilityReport-PageTab.jpeg differ