diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9685ebb
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,284 @@
+# XPRTZ Websites Monorepo - Agent Context
+
+## Project Overview
+This is a monorepo for XPRTZ websites built with Astro, consuming content from a Strapi CMS. It uses npm workspaces to manage multiple applications and shared libraries.
+
+## Monorepo Structure
+
+```
+/website
+├── apps/
+│ ├── dotnet/ # Main .NET-focused website (xprtz.net)
+│ └── learning/ # Learning platform for polyglot programming
+├── libs/
+│ ├── ui/ # Shared Astro/React UI components
+│ └── cms/ # TypeScript types and API wrapper for Strapi
+├── infrastructure/ # Infrastructure configuration
+└── package.json # Root workspace configuration
+```
+
+## Workspace Configuration
+
+### npm Workspaces
+The monorepo uses npm workspaces defined in the root [package.json](package.json):
+
+```json
+"workspaces": [
+ "apps/dotnet",
+ "apps/learning",
+ "libs/ui",
+ "libs/cms"
+]
+```
+
+### Package Naming Convention
+- Apps: `@xprtz/dotnet`, `@xprtz/learning`
+- Libraries: `@xprtz/ui`, `@xprtz/cms`
+
+### Dependency Pattern
+Both apps depend on shared libraries using local file references:
+```json
+"dependencies": {
+ "@xprtz/ui": "file:../../libs/ui",
+ "@xprtz/cms": "file:../../libs/cms"
+}
+```
+
+## Technology Stack
+
+### Core Technologies
+- **Astro 5.16+** - Static site generator with islands architecture
+- **React 18.3+** - Client-side interactivity
+- **TypeScript 5.4+** - Type safety across the monorepo
+- **Tailwind CSS 3.4+** - Utility-first CSS framework
+- **Strapi CMS** - Headless CMS for content management
+
+### Key Dependencies
+- `@astrojs/tailwind` - Tailwind integration
+- `@astrojs/react` - React integration for interactive components
+- `@astrojs/sitemap` - Sitemap generation
+- `@headlessui/react` - Unstyled, accessible UI components
+- `@heroicons/react` - Icon library
+- `embla-carousel` - Carousel/slider library
+- `marked` - Markdown parser
+
+## File Naming Conventions
+
+### General Rules
+- **Astro components**: `PascalCase.astro` (e.g., `Hero.astro`, `Footer.astro`)
+- **React components**: `PascalCase.tsx` (e.g., `Header.tsx`)
+- **TypeScript files**: `camelCase.ts` (e.g., `api.ts`, `page.ts`)
+- **Configuration files**: Standard names (`package.json`, `astro.config.mjs`, `tsconfig.json`)
+
+### Specific Conventions
+- CMS models: `camelCase.ts` in `libs/cms/models/`
+- Page components: File-based routing in `apps/*/src/pages/`
+- Layout files: `camelCase.astro` in `apps/*/src/layouts/`
+- Dynamic routes: `[param].astro` or `[...slug].astro`
+
+## Multi-Tenant Architecture
+
+The system supports multiple sites using the `PUBLIC_SITE` environment variable:
+
+### Site Identifiers
+- `"dotnet"` - Main .NET-focused website (xprtz.net)
+- `"learning"` - Learning platform for polyglot programming
+
+### Content Filtering
+All CMS queries filter content by site:
+```typescript
+query: {
+ "filters[site][$eq]": import.meta.env.PUBLIC_SITE,
+ status: "published"
+}
+```
+
+## Environment Variables
+
+### Required Variables
+- `PUBLIC_SITE` - Site identifier ("dotnet" or "learning")
+- `PUBLIC_STRAPI_URL` - Strapi CMS API base URL
+- `PUBLIC_IMAGES_URL` - Image CDN base URL
+
+### Usage Pattern
+```typescript
+const site = import.meta.env.PUBLIC_SITE || "no-site-found";
+const imagesUrl = import.meta.env.PUBLIC_IMAGES_URL;
+```
+
+## Development Scripts
+
+### Root Level Commands
+```bash
+npm run develop:dotnet # Start dotnet app dev server (port 3001)
+npm run develop:learning # Start learning app dev server
+npm run build # Build all workspaces
+npm run build:dotnet # Build dotnet app only
+npm run build:learning # Build learning app only
+npm run build:ui # Type-check UI library
+npm run format # Format and lint all files
+```
+
+### Individual Workspace Commands
+```bash
+cd apps/dotnet
+npm run develop # Start dev server
+npm run build # Type-check and build
+npm run preview # Preview production build
+```
+
+## TypeScript Configuration
+
+### Root tsconfig.json
+Provides shared TypeScript configuration for all workspaces with strict mode enabled.
+
+### Import Path Conventions
+- Use `.js` extensions in imports (TypeScript ESM requirement)
+- Type-only imports: `import { type Hero } from "@xprtz/cms"`
+- Component imports: `import { ComponentRenderer } from "@xprtz/ui"`
+
+## Coding Standards
+
+### Code Style
+- Prettier for formatting (config in [.prettierrc.json](.prettierrc.json))
+- ESLint for linting (config in [eslint.config.mjs](eslint.config.mjs))
+- TypeScript strict mode enabled
+
+### Import Organization
+1. External dependencies
+2. Workspace packages (`@xprtz/*`)
+3. Relative imports
+
+### Naming Conventions
+- **Functions**: `camelCase` (e.g., `fetchData`, `formatDate`)
+- **Types**: `PascalCase` (e.g., `Hero`, `Article`, `Page`)
+- **Constants**: `UPPER_SNAKE_CASE` (e.g., `CAROUSEL_CONFIG`)
+- **CSS classes**: Tailwind utilities + custom kebab-case
+
+## Git Workflow
+
+### Recent Activity
+- Current branch: `feature/technology-radar`
+- Recent commits focus on radar chart animation improvements and accessibility enhancements
+- Fixed tile zoom and list slide animations for mobile view
+
+## Astro-Specific Configuration
+
+### Hot Module Replacement (HMR) for Workspace Dependencies
+The Astro dev server may not properly hot-reload CSS changes from workspace dependencies (`@xprtz/ui`, `@xprtz/cms`). This has been resolved by configuring Vite in each app's `astro.config.mjs`:
+
+```js
+export default defineConfig({
+ vite: {
+ server: {
+ watch: {
+ // Force Vite to watch workspace dependencies
+ ignored: ['!**/node_modules/@xprtz/ui/**']
+ }
+ },
+ optimizeDeps: {
+ // Prevent pre-bundling of workspace dependencies
+ exclude: ['@xprtz/ui']
+ }
+ }
+})
+```
+
+This configuration ensures that changes to UI library files trigger proper hot reloads without requiring dev server restarts.
+
+## Radar Chart Component
+
+### Overview
+The Technology Radar chart ([RadarChart.astro](libs/ui/src/radar/RadarChart.astro)) is a complex interactive component that displays technology items organized by quadrants and rings. It has different behaviors for desktop and mobile viewports.
+
+### Architecture
+- **Desktop (>1000px)**: Full radar chart with quadrants that can zoom
+- **Mobile (≤1000px)**: Colored tiles representing each quadrant
+
+### Animation System
+
+#### Desktop Radar Animations
+1. **Hover Effect**: Non-hovered quadrants dim to 50% opacity
+2. **Zoom In**:
+ - Clicked quadrant scales to 1.75x and centers
+ - Other quadrants fade to opacity 0
+ - Ring labels transform with the zoomed quadrant
+ - Duration: 500ms
+3. **List Slide**:
+ - After zoom, wrapper translates left by 200px
+ - Item list slides in from right
+ - Duration: 400ms
+
+#### Mobile Tile Animations
+The mobile animations were redesigned to create smooth cross-slide transitions:
+
+1. **Opening (Tile → List)**:
+ - Tile zooms in via `tileZoomIn` keyframe (scale 2.05) - 500ms
+ - After zoom: Tile slides left AND fades out while list slides in from right - 400ms
+ - Both elements pass each other smoothly
+ - After slide completes, tile becomes `display: none`
+
+2. **Closing (List → Tile)**:
+ - Tile positioned off-screen left with opacity 0
+ - List slides right while tile slides in from left AND fades in - 400ms
+ - After slide: Tile zooms out via `tileZoomOut` keyframe - 500ms
+
+#### Key CSS Classes for Mobile
+- `.sliding-out`: Tiles slide left with `translateX(-100%)` and fade to `opacity: 0`. For lists, triggers `slideOut` keyframe animation
+- `.sliding-in`: Tiles positioned at `translateX(-100%)` ready to slide in
+- `.zooming-in`: Triggers `tileZoomIn` keyframe animation on tiles
+- `.zooming-out`: Triggers `tileZoomOut` keyframe animation on tiles
+- `.list-visible`: Triggers list `slideIn` keyframe animation
+- `.list-animation-complete`: Switches list to `position: static` after animation
+
+#### Animation Implementation
+All mobile animations use CSS **keyframe animations** instead of transitions for better mobile compatibility:
+- **tileZoomIn keyframe**: Animates tile from `scale(1)` to `scale(2.05)` - defined in [RadarChart.astro](libs/ui/src/radar/RadarChart.astro)
+- **tileZoomOut keyframe**: Animates tile from `scale(2.05)` to `scale(1)` - defined in [RadarChart.astro](libs/ui/src/radar/RadarChart.astro)
+- **slideIn keyframe**: Animates list from `translateX(100%)` opacity 0 to `translateX(0)` opacity 1 - defined in [RadarQuadrantItemList.astro](libs/ui/src/radar/RadarQuadrantItemList.astro)
+- **slideOut keyframe**: Animates list from `translateX(0)` opacity 1 to `translateX(100%)` opacity 0 - defined in [RadarQuadrantItemList.astro](libs/ui/src/radar/RadarQuadrantItemList.astro)
+- Keyframes are more reliable on mobile devices than CSS transitions
+
+#### Animation Timing Variables
+Defined in [RadarChart.astro:200-205](libs/ui/src/radar/RadarChart.astro#L200-L205):
+```css
+--fade-out-duration: 300ms;
+--fade-out-delay: 100ms;
+--fade-in-duration: 400ms;
+--zoom-duration: 500ms;
+--slide-duration: 400ms;
+--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
+```
+
+### Important Constraints
+1. **DO NOT** change the `position: static` rule for `.list-animation-complete` on mobile - this is required to prevent content overlap
+2. List positioning uses `left: 50%` with `margin-left: -192px` (half of 384px width) to ensure proper centering during animation
+
+### Related Components
+- [RadarQuadrant.astro](libs/ui/src/radar/RadarQuadrant.astro) - Individual quadrant rendering
+- [RadarQuadrantItemList.astro](libs/ui/src/radar/RadarQuadrantItemList.astro) - Item list with slide animations
+- [radarUtils.ts](libs/ui/src/radar/radarUtils.ts) - Shared constants and utilities
+
+## Important Notes
+
+### When Adding New Features
+1. UI components go in `libs/ui/src/`
+2. CMS models go in `libs/cms/models/`
+3. Export new components/types from respective `index.ts` files
+4. Register new UI components in `ComponentRenderer.astro` if they're CMS-driven
+5. Follow existing patterns for file naming and code structure
+
+### When Adding New Content Types
+1. Create type definition in `libs/cms/models/`
+2. Export from `libs/cms/index.ts`
+3. Create corresponding Astro component in `libs/ui/src/`
+4. Export from `libs/ui/index.ts`
+5. Add to `ComponentRenderer.astro` mapping if applicable
+6. Ensure Strapi CMS has matching content type
+
+### Development Workflow
+1. Start with understanding the CMS model structure
+2. Create or update TypeScript types in `libs/cms`
+3. Create or update UI components in `libs/ui`
+4. Use components in apps via `ComponentRenderer` or direct imports
+5. Test with both dotnet and learning sites to ensure multi-tenant compatibility
diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md
new file mode 100644
index 0000000..a13c30d
--- /dev/null
+++ b/DEVELOPMENT_GUIDE.md
@@ -0,0 +1,451 @@
+# XPRTZ Websites Development Guide
+
+## Quick Reference
+
+This guide provides a quick overview of the codebase structure and common development tasks. For detailed information, see the AGENTS.md files in each directory.
+
+## Architecture Overview
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Strapi CMS │
+│ (Multi-tenant content) │
+└─────────────────────────────────────────────────────────┘
+ │
+ │ API calls (fetchData)
+ ▼
+┌─────────────────────────────────────────────────────────┐
+│ @xprtz/cms (libs/cms) │
+│ - TypeScript types for CMS content │
+│ - API wrapper (fetchData function) │
+│ - Multi-tenant & locale support │
+└─────────────────────────────────────────────────────────┘
+ │
+ │ Import types
+ ▼
+┌─────────────────────────────────────────────────────────┐
+│ @xprtz/ui (libs/ui) │
+│ - Reusable Astro/React components │
+│ - ComponentRenderer (dynamic rendering) │
+│ - Tailwind-styled components │
+└─────────────────────────────────────────────────────────┘
+ │
+ │ Import components
+ ▼
+ ┌──────────────────┴──────────────────┐
+ ▼ ▼
+┌──────────────────┐ ┌──────────────────┐
+│ @xprtz/dotnet │ │ @xprtz/learning │
+│ (apps/dotnet) │ │ (apps/learning) │
+│ │ │ │
+│ - xprtz.net │ │ - Learning app │
+│ - Pages & Blog │ │ - Tutorials │
+└──────────────────┘ └──────────────────┘
+```
+
+## Key Concepts
+
+### 1. Multi-Tenant System
+Both apps share CMS and UI libraries but filter content by `PUBLIC_SITE`:
+- `dotnet` app uses `PUBLIC_SITE="dotnet"`
+- `learning` app uses `PUBLIC_SITE="learning"`
+
+### 2. Component-Driven Architecture
+CMS content includes a `__component` field that maps to UI components via `ComponentRenderer`:
+```
+CMS: { __component: "ui.hero", ... } → UI: Hero.astro
+```
+
+### 3. Static Site Generation
+All pages are generated at build time using `getStaticPaths()` from CMS data.
+
+## Common Development Tasks
+
+### Task 1: Add a New UI Component
+
+**Example: Adding a "Testimonial" component**
+
+1. **Create CMS Type** (`libs/cms/models/testimonial.ts`)
+ ```typescript
+ import { Image } from "./image.js";
+
+ export type Testimonial = {
+ __component: "ui.testimonial";
+ id: number;
+ quote: string;
+ author: string;
+ title: string;
+ avatar: Image;
+ }
+ ```
+
+2. **Export from CMS** (`libs/cms/index.ts`)
+ ```typescript
+ import { Testimonial } from "./models/testimonial.js";
+ export { type Testimonial };
+ ```
+
+3. **Create Astro Component** (`libs/ui/src/Testimonial.astro`)
+ ```astro
+ ---
+ import { type Testimonial } from "@xprtz/cms";
+ const testimonial = Astro.props as Testimonial;
+ const site = import.meta.env.PUBLIC_IMAGES_URL;
+ ---
+
+
+ "{testimonial.quote}"
+
+
+
+
+
{testimonial.author}
+
{testimonial.title}
+
+
+
+ ```
+
+4. **Export from UI** (`libs/ui/index.ts`)
+ ```typescript
+ import Testimonial from "./src/Testimonial.astro";
+ export { Testimonial };
+ ```
+
+5. **Register in ComponentRenderer** (`libs/ui/src/ComponentRenderer.astro`)
+ ```typescript
+ import Testimonial from "./Testimonial.astro";
+
+ const componentMap: Record = {
+ // ... existing mappings
+ "ui.testimonial": Testimonial,
+ };
+ ```
+
+6. **Create in Strapi CMS**
+ - Create component with identifier: `ui.testimonial`
+ - Add fields: quote (text), author (text), title (text), avatar (media)
+ - Add to page components zone
+
+### Task 2: Add a New Page to Dotnet App
+
+**Example: Adding a "Services" page**
+
+1. **Create Page in Strapi**
+ - Content Type: Page
+ - Set slug: `services`
+ - Set site: `dotnet`
+ - Add components as needed
+
+2. **Page is Automatically Generated**
+ - The dynamic route `apps/dotnet/src/pages/[slug].astro` will generate it
+ - Access at: `/services`
+
+3. **Or Create Static Page** (`apps/dotnet/src/pages/services.astro`)
+ ```astro
+ ---
+ import Layout from '../layouts/layout.astro';
+ import { fetchData, type Page } from '@xprtz/cms';
+ import { ComponentRenderer } from '@xprtz/ui';
+
+ const site = import.meta.env.PUBLIC_SITE || "dotnet";
+
+ const pages = await fetchData>({
+ endpoint: "pages",
+ wrappedByKey: "data",
+ wrappedByList: true,
+ query: {
+ "filters[slug][$eq]": "services",
+ "filters[site][$eq]": site,
+ "populate": "deep",
+ },
+ });
+
+ const page = pages[0];
+ ---
+
+
+
+
+ ```
+
+### Task 3: Update Existing Component
+
+**Example: Adding a subtitle field to Hero**
+
+1. **Update CMS Type** (`libs/cms/models/hero.ts`)
+ ```typescript
+ export type Hero = {
+ __component: "ui.hero";
+ id: number;
+ title: string;
+ subtitle: string; // NEW FIELD
+ description: string;
+ CTO: Link;
+ link: Link;
+ images: Image[];
+ }
+ ```
+
+2. **Update Component** (`libs/ui/src/Hero.astro`)
+ ```astro
+ {hero.title}
+ {hero.subtitle}
// NEW
+ {hero.description}
+ ```
+
+3. **Update Strapi**
+ - Add `subtitle` field to Hero component in Strapi
+ - Update existing content
+
+### Task 4: Create a Custom Page Layout
+
+**Example: Adding a two-column layout page**
+
+1. **Create CMS Type** (`libs/cms/models/twoColumnPage.ts`)
+ ```typescript
+ export type TwoColumnPage = {
+ __component: "ui.two-column-page";
+ id: number;
+ leftColumn: any[]; // Array of components
+ rightColumn: any[]; // Array of components
+ }
+ ```
+
+2. **Create Component** (`libs/ui/src/TwoColumnPage.astro`)
+ ```astro
+ ---
+ import { type TwoColumnPage } from "@xprtz/cms";
+ import ComponentRenderer from "./ComponentRenderer.astro";
+
+ const data = Astro.props as TwoColumnPage;
+ ---
+
+ ```
+
+3. **Export and Register** (follow steps 2, 4, 5 from Task 1)
+
+### Task 5: Fetch and Display Related Content
+
+**Example: Showing related articles on article pages**
+
+1. **Update Article Page** (`apps/dotnet/src/pages/artikelen/[article].astro`)
+ ```astro
+ ---
+ const article: Article = Astro.props;
+
+ // Fetch related articles by tags
+ const relatedArticles = await fetchData>({
+ endpoint: "articles",
+ wrappedByKey: "data",
+ query: {
+ "filters[site][$eq]": "dotnet",
+ "filters[tags][name][$in]": article.tags.map(t => t.name).join(","),
+ "filters[slug][$ne]": article.slug, // Exclude current article
+ "pagination[limit]": "3",
+ status: "published",
+ },
+ });
+ ---
+
+
+
+
+
+
+ ```
+
+## File Location Reference
+
+| What | Where | Example |
+|------|-------|---------|
+| CMS Type Definition | `libs/cms/models/` | `libs/cms/models/hero.ts` |
+| UI Component | `libs/ui/src/` | `libs/ui/src/Hero.astro` |
+| App Page | `apps/dotnet/src/pages/` | `apps/dotnet/src/pages/index.astro` |
+| App Layout | `apps/dotnet/src/layouts/` | `apps/dotnet/src/layouts/layout.astro` |
+| Static Assets | `apps/dotnet/src/assets/` | `apps/dotnet/src/assets/logo.svg` |
+| Public Files | `apps/dotnet/public/` | `apps/dotnet/public/favicon.ico` |
+
+## Naming Conventions Quick Reference
+
+| Type | Convention | Example |
+|------|------------|---------|
+| Astro Component | PascalCase.astro | `Hero.astro` |
+| React Component | PascalCase.tsx | `Header.tsx` |
+| TypeScript Type | PascalCase | `Hero`, `Article` |
+| TypeScript File | camelCase.ts | `hero.ts`, `api.ts` |
+| Function | camelCase | `fetchData`, `formatDate` |
+| Constant | UPPER_SNAKE_CASE | `CAROUSEL_CONFIG` |
+| CSS Class | kebab-case | `team-member`, `blog-card` |
+| Component ID | ui.kebab-case | `ui.hero`, `ui.testimonial` |
+
+## Environment Variables
+
+### Development (.env files)
+
+**apps/dotnet/.env**
+```env
+PUBLIC_SITE=dotnet
+PUBLIC_STRAPI_URL=https://your-strapi-url.com
+PUBLIC_IMAGES_URL=https://your-cdn-url.com
+```
+
+**apps/learning/.env**
+```env
+PUBLIC_SITE=learning
+PUBLIC_STRAPI_URL=https://your-strapi-url.com
+PUBLIC_IMAGES_URL=https://your-cdn-url.com
+```
+
+## Development Workflow
+
+### Starting Development
+```bash
+# From root
+npm run develop:dotnet # Start dotnet app (http://localhost:3001)
+npm run develop:learning # Start learning app
+
+# Or from specific app
+cd apps/dotnet
+npm run develop
+```
+
+### Making Changes
+
+1. **Change in UI component?**
+ - Edit file in `libs/ui/src/`
+ - Changes are immediately reflected in both apps (no rebuild needed)
+
+2. **Change in CMS type?**
+ - Edit file in `libs/cms/models/`
+ - Export from `libs/cms/index.ts`
+ - Changes are immediately reflected (no rebuild needed)
+
+3. **Change in app page?**
+ - Edit file in `apps/dotnet/src/pages/`
+ - Dev server will hot-reload
+
+### Building for Production
+```bash
+# Build everything
+npm run build
+
+# Build specific app
+npm run build:dotnet
+npm run build:learning
+
+# Type-check UI library
+npm run build:ui
+```
+
+## Debugging Tips
+
+### CMS Data Not Showing
+1. Check `PUBLIC_SITE` environment variable
+2. Verify Strapi content has correct `site` field
+3. Check `status: "published"` in query
+4. Use `populate: "deep"` to get all relations
+5. Log the fetched data to console
+
+### Component Not Rendering
+1. Verify component is exported from `libs/ui/index.ts`
+2. Check `__component` value matches `ComponentRenderer` mapping
+3. Ensure Strapi component identifier matches exactly
+4. Check browser console for errors
+
+### TypeScript Errors
+1. Run `npm run build:ui` to check UI types
+2. Run `npm run build:dotnet` to check app types
+3. Ensure imports use `.js` extensions
+4. Verify types are exported from `libs/cms/index.ts`
+
+### Styling Issues
+1. Check Tailwind classes are spelled correctly
+2. Verify custom classes are defined in component `
+
+
+
+```
+
+### Props Pattern
+Components receive props via `Astro.props` and cast to the appropriate CMS type:
+
+```typescript
+import { type Hero } from "@xprtz/cms";
+const hero = Astro.props as Hero;
+```
+
+### Image Handling
+Images use the `PUBLIC_IMAGES_URL` environment variable:
+
+```astro
+const site = import.meta.env.PUBLIC_IMAGES_URL;
+
+```
+
+### Client-Side Interactivity
+Use React components with `client:load` directive for client-side hydration:
+
+```astro
+
+```
+
+## ComponentRenderer Pattern
+
+The [ComponentRenderer.astro](src/ComponentRenderer.astro) is crucial for CMS-driven content. It maps Strapi component identifiers to actual Astro components:
+
+```typescript
+const componentMap: Record = {
+ "ui.text": Text,
+ "ui.hero": Hero,
+ "ui.missie-met-statistieken": Mission,
+ "ui.kernwaarden": Values,
+ "ui.opsomming": Listing,
+ "ui.titel": SubtitleWithText,
+ "ui.quote": Quote,
+ "ui.image": HomePageImage,
+ "ui.image-met-titel": HomePageImageWithTitle,
+ "ui.page-image": PageImage,
+ "ui.artikelen": Blogs,
+ "ui.artikelen-overzicht": BlogListing,
+ "ui.klant-logo-s": LogoCloud,
+ "ui.team": TeamCarousel,
+ "ui.directeuren": Directors,
+ "ui.technology-radar": TechnologyRadar,
+};
+```
+
+### Usage
+```astro
+
+```
+
+This dynamically renders all components from CMS data based on their `__component` field.
+
+## Styling Approach
+
+### Tailwind CSS
+Primary styling method using utility classes:
+
+```astro
+
+ {hero.title}
+
+```
+
+### Custom Theme Colors
+Components use a `primary-*` color palette:
+- `primary-100` to `primary-900` - Brand color scale
+- `text-primary-800` - Primary text color
+- `bg-primary-600` - Primary background color
+
+### Responsive Design
+Tailwind breakpoint prefixes:
+- `sm:` - Small screens and up
+- `md:` - Medium screens and up
+- `lg:` - Large screens and up
+- `xl:` - Extra large screens and up
+
+### Scoped Styles
+Used for complex layouts (e.g., carousels):
+
+```astro
+
+```
+
+## Dependencies
+
+### Key Dependencies
+- `astro` - Astro framework
+- `react` + `react-dom` - React library
+- `@astrojs/react` - React integration
+- `@headlessui/react` - Accessible UI components
+- `@heroicons/react` - Icon library
+- `embla-carousel` - Carousel library
+- `marked` - Markdown to HTML parser
+- `@xprtz/cms` - Types imported but NOT listed in package.json (provided by parent workspace)
+
+### Important Notes
+- The CMS library is NOT a dependency in package.json
+- CMS types are imported directly from the workspace
+- Components assume CMS types are available via workspace resolution
+
+## Export Pattern
+
+All components are exported from [index.ts](index.ts):
+
+```typescript
+import Hero from "./src/Hero.astro";
+import Header from "./src/Header.tsx";
+// ... other imports
+
+export {
+ Hero,
+ Header,
+ ComponentRenderer,
+ // ... other exports
+};
+```
+
+### Import Usage in Apps
+```typescript
+import { Hero, Header, ComponentRenderer } from "@xprtz/ui";
+```
+
+## Adding New Components
+
+### Step-by-Step Process
+
+1. **Create CMS Type** (in `@xprtz/cms`)
+ ```typescript
+ // libs/cms/models/newComponent.ts
+ export type NewComponent = {
+ __component: "ui.new-component";
+ id: number;
+ title: string;
+ // ... other fields
+ };
+ ```
+
+2. **Create Astro Component**
+ ```astro
+ ---
+ // libs/ui/src/NewComponent.astro
+ import { type NewComponent } from "@xprtz/cms";
+ const data = Astro.props as NewComponent;
+ ---
+
+
{data.title}
+
+ ```
+
+3. **Export from index.ts**
+ ```typescript
+ import NewComponent from "./src/NewComponent.astro";
+ export { NewComponent };
+ ```
+
+4. **Register in ComponentRenderer** (if CMS-driven)
+ ```typescript
+ const componentMap: Record = {
+ // ... existing mappings
+ "ui.new-component": NewComponent,
+ };
+ ```
+
+5. **Ensure Strapi has matching content type**
+ - Component identifier must match: `ui.new-component`
+ - Fields must match TypeScript type definition
+
+## Component Development Guidelines
+
+### TypeScript Type Safety
+- Always import and use CMS types
+- Use type assertions: `const data = Astro.props as ComponentType`
+- Never use `any` unless absolutely necessary
+
+### Astro vs React Components
+- **Use Astro** for static/server-side rendering (default)
+- **Use React** when you need:
+ - State management (`useState`, `useEffect`)
+ - Client-side interactivity
+ - Event handlers
+ - Form interactions
+
+### Image Best Practices
+- Always use `alternateText` for accessibility
+- Prefix image URLs with `PUBLIC_IMAGES_URL`
+- Use responsive image techniques (aspect ratios, object-fit)
+
+### Accessibility
+- Use semantic HTML elements
+- Include ARIA labels where needed
+- Ensure keyboard navigation works
+- Test with screen readers
+
+### Performance
+- Minimize client-side JavaScript
+- Use Astro components by default (zero JS shipped)
+- Only hydrate with React when necessary (`client:load`, `client:visible`)
+- Optimize images (proper formats, sizes)
+
+## Testing
+
+### Type Checking
+```bash
+npm run build # Runs astro check
+```
+
+### Manual Testing
+- Test in both `dotnet` and `learning` apps
+- Verify responsive behavior at all breakpoints
+- Check dark/light mode if applicable
+- Validate accessibility
+
+## Common Patterns
+
+### Conditional Rendering
+```astro
+{data.description && (
+ {data.description}
+)}
+```
+
+### Mapping Arrays
+```astro
+{items.map((item) => (
+ {item.title}
+))}
+```
+
+### Environment Variables
+```astro
+const site = import.meta.env.PUBLIC_IMAGES_URL;
+```
+
+### Link Handling
+```astro
+
+ {link.title}
+
+```
+
+## Carousel Pattern (Embla)
+
+Components like [TeamCarousel.astro](src/TeamCarousel.astro) use Embla Carousel:
+
+```astro
+
+```
+
+## Technology Radar Pattern
+
+The Technology Radar components provide an interactive visualization of technology adoption. The radar can be added to any CMS-managed page using the `TechnologyRadar` component.
+
+### TechnologyRadar Component (CMS-Driven)
+
+The main CMS component that can be added to any page to display the technology radar.
+
+**Props:**
+- `__component: "ui.technology-radar"` - Component identifier for CMS
+- `title: string` - Title displayed above the radar
+
+**Features:**
+- Automatically fetches all radar items from CMS
+- Renders the complete radar visualization using `RadarChart`
+- Includes tag filtering functionality with toggle buttons
+- Displays all unique tags from radar items
+- Clicking a tag filters radar items to show only those with that tag
+- Clicking the same tag again removes the filter
+- Smooth opacity transitions for filtered items
+- **Hover highlighting**: When hovering over an item in the quadrant list, the corresponding radar item on the chart is highlighted with enhanced visual effects (larger size, color fill, and glow)
+- **Tag filtering**: Stores tag information in `data-tags` attributes on radar items for client-side filtering
+- Uses `initializeOnReady` utility from radarUtils for consistent initialization across page loads
+
+**Usage in CMS:**
+Add the component to any page's components array in Strapi with:
+- Component type: `ui.technology-radar`
+- Title field: e.g., "Our Technology Radar"
+
+**Direct Usage (if needed):**
+```astro
+import { TechnologyRadar } from "@xprtz/ui";
+
+
+```
+
+**Important Notes:**
+- The component handles its own data fetching - no need to pass items
+- Tag filtering is client-side and persists until page reload
+- Links to radar items point to `/radar-items/{slug}` with a `from` query parameter
+- The `from` parameter contains the current page slug for proper back navigation
+- This replaces the old hard-coded expertise page approach
+
+**Back Navigation:**
+- The component automatically detects the current page URL
+- When users click on a radar item, the current page slug is passed as `?from={slug}`
+- The radar item page uses this to create a dynamic "Back" link
+- If no `from` parameter exists, it defaults to `/expertise` for backwards compatibility
+
+### RadarChart Component
+
+The main radar visualization that combines four quadrants into a complete circle with full responsive design support.
+
+**Props:**
+- `items?: RadarItem[]` - Array of radar items from CMS
+- `quadrantSize?: number` - Size of each quadrant in pixels (default: 400)
+- `parentPageSlug?: string` - Slug of the parent page for back navigation (optional)
+
+**Features:**
+- Automatically numbers items sequentially (starting at 1)
+- Distributes items to appropriate quadrants based on `item.quadrant` field
+- Uses predefined colors for each quadrant:
+ - **Techniques**: Blue (#3b82f6)
+ - **Tools**: Green (#10b981)
+ - **Platforms**: Amber (#f59e0b)
+ - **Languages & Frameworks**: Red (#ef4444)
+- 20px margin between quadrants for visual separation
+- Interactive zoom functionality when clicking quadrants
+- Cross-highlighting: hovering list items highlights corresponding radar items
+
+**Responsive Design - Three Distinct States:**
+
+The RadarChart has three distinct view states. **IMPORTANT: Animations for one state must NOT affect animations in other states.**
+
+1. **Full Chart State (> 1024px width)**
+ - Full radar chart with four quadrants displayed
+ - Hover effects dim non-hovered quadrants to 50% opacity
+ - Click to zoom individual quadrants (1.75x scale)
+ - Zoomed quadrant transforms to center-left position
+ - Ring labels (Adopt, Trial, Assess, Hold) transform with zoomed quadrant
+ - Item list appears to the right of the zoomed quadrant
+
+2. **Tiles State (≤ 1024px and > 360px width)**
+ - Radar chart hidden (`display: none`)
+ - Four colored tiles displayed in 2x2 grid
+ - Each tile represents a quadrant with matching colors
+ - Hover effects dim non-hovered tiles to 50% opacity
+ - Click shows item list, hides tiles
+
+3. **Single Column Tiles State (≤ 360px width)**
+ - Tiles stack in single column (1x4 grid)
+ - Hover dim effects disabled (doesn't make sense for single column)
+ - Click shows item list, hides tiles
+
+**Animation Rules:**
+
+1. **Always use CSS keyframes for animations** - CSS transitions have caused issues on mobile devices. All animations must be implemented using `@keyframes`.
+
+2. **State isolation** - Animations defined for one state (Full Chart, Tiles, Single Column) must NOT affect other states. Use appropriate media query scoping.
+
+**State Management:**
+
+The component uses CSS classes to manage states:
+- `list-visible` - When item list is shown
+- `zoomed` - Applied to radar chart when any quadrant is zoomed
+- `zoom-{quadrant}` - Applied alongside `zoomed` to identify which quadrant (e.g., `zoom-tools`)
+- `zoomed-in` - Applied to the specific quadrant being zoomed
+- `visible` - Applied to item list to show it
+- `hovering` - Applied during hover interactions
+- `active` - Applied to the hovered element
+
+**Initialization:**
+
+The component uses the `initializeOnReady` utility function from `radarUtils.ts` for consistent initialization:
+- Handles both Astro page-load events and initial page load
+- Ensures initialization runs exactly once per page load
+- Shared pattern across all radar components
+
+**Usage:**
+```astro
+import { RadarChart } from "@xprtz/ui";
+
+
+```
+
+### RadarQuadrant Component
+
+Individual quadrant representing 90° of the radar (one quarter circle).
+
+**Props:**
+- `gridPosition: "top-left" | "top-right" | "bottom-left" | "bottom-right"` - Position in the 2x2 grid layout
+- `color: string` - Color for the quadrant
+- `size?: number` - Size in pixels (default: 400)
+- `items?: RadarItemWithNumber[]` - Radar items to display
+- `parentPageSlug?: string` - Slug of the parent page for back navigation (optional)
+
+**Features:**
+- Four concentric rings representing adoption stages (defined in `radarUtils.ts`):
+ - **Adopt** (innermost, 25% radius)
+ - **Trial** (50% radius)
+ - **Assess** (75% radius)
+ - **Hold** (outermost, 100% radius)
+- Items displayed as numbered circles (12px radius)
+- Deterministic positioning using golden angle distribution
+- Interactive items with:
+ - SVG tooltips showing "{number}. {title}"
+ - Click to navigate to item detail page
+ - Hover effects (gray background on hover)
+ - Highlight effects when item in list is hovered (colored fill, larger size, glow)
+- Origin point positioned at appropriate corner based on grid position
+- **Tag data**: Each radar item includes a `data-tags` attribute with JSON-serialized tag titles for filtering
+
+**Item Positioning Algorithm:**
+- Items placed within their ring based on `item.ring` value
+- Pseudo-random but deterministic distribution using item number as seed
+- Golden angle (137.508°) for optimal spread
+- Radius variation within ring (30-90% of ring width) to reduce overlap
+- Optimized for 20-30 items per quadrant
+
+**Item Links:**
+- Items link to `/radar-items/{slug}` for individual radar item pages
+- Uses `buildRadarItemLink` utility from `radarUtils.ts` for consistent link generation
+- If `parentPageSlug` is provided, adds `?from={parentPageSlug}` query parameter for back navigation
+- The radar item page reads this parameter to create a dynamic back link
+
+**Usage:**
+```astro
+import { RadarQuadrant } from "@xprtz/ui";
+
+
+```
+
+### RadarItem Types
+
+The radar system uses two related types:
+
+```typescript
+// Base type from CMS (no number property)
+type RadarItem = {
+ slug: string;
+ quadrant: RadarQuadrant;
+ ring: RadarRing;
+ title: string;
+ description: string;
+ pros: ListItem[];
+ cons: ListItem[];
+ conclusion: string;
+ tags: Tag[];
+}
+
+// Extended type with number property (added by UI layer)
+type RadarItemWithNumber = RadarItem & {
+ number: number; // Sequential number (1, 2, 3...)
+}
+```
+
+**Type Usage Flow:**
+1. Fetch `RadarItem[]` from CMS (no numbers)
+2. RadarChart component adds numbers, creating `RadarItemWithNumber[]`
+3. Pass `RadarItemWithNumber[]` to child components (RadarQuadrant, RadarQuadrantItemList)
+
+### RadarQuadrantItemList Component
+
+List view displaying all items within a selected quadrant, grouped by ring.
+
+**Props:**
+- `items: RadarItemWithNumber[]` - Array of radar items for the quadrant
+- `quadrantName: string` - Name of the quadrant
+- `color: string` - Color for the quadrant header
+- `parentPageSlug?: string` - Slug of the parent page for back navigation (optional)
+
+**Features:**
+- Groups items by ring (Adopt, Trial, Assess, Hold) using `RINGS` constant from `radarUtils.ts`
+- Displays item number, title, and description
+- Links to item detail pages with `from` query parameter using `buildRadarItemLink` utility
+- Scrollable list with sticky ring headers
+- "Back to radar" button to return to main view
+- Hidden by default (`display: none`), shown when quadrant is clicked (`.visible` class)
+- **Interactive hover**: Hovering over list items highlights the corresponding radar item on the chart
+- Custom scrollbar styling for better UX in scrollable content
+
+### Shared Utilities (radarUtils.ts)
+
+The radar components share common utilities defined in `radarUtils.ts`:
+
+**Constants:**
+```typescript
+export const RINGS: readonly { readonly label: RadarRing; readonly radiusPosition: number }[] = [
+ { label: "Adopt", radiusPosition: 25 },
+ { label: "Trial", radiusPosition: 50 },
+ { label: "Assess", radiusPosition: 75 },
+ { label: "Hold", radiusPosition: 100 },
+] as const;
+```
+- Single source of truth for ring configuration
+- Used by RadarChart, RadarQuadrant, and RadarQuadrantItemList
+- Ensures consistent ring ordering and positioning across all components
+
+**Functions:**
+
+`buildRadarItemLink(slug: string, parentPageSlug?: string): string`
+- Builds consistent links to radar item pages
+- Adds optional `?from={parentPageSlug}` query parameter for back navigation
+- Used by RadarQuadrant and RadarQuadrantItemList
+
+`initializeOnReady(callback: () => void): void`
+- Handles both Astro page-load events and initial page load
+- Ensures callback runs exactly once when page is ready
+- Used by RadarChart and TechnologyRadar for consistent initialization
+
+### Integration Example
+
+**Using TechnologyRadar (Recommended for CMS pages):**
+```astro
+---
+import { TechnologyRadar } from "@xprtz/ui";
+---
+
+
+
+```
+
+**Using RadarChart directly (Advanced usage):**
+```astro
+---
+import { fetchData, type RadarItem } from "@xprtz/cms";
+import { RadarChart } from "@xprtz/ui";
+
+const allRadarItems = await fetchData>({
+ endpoint: "radar-items",
+ wrappedByKey: "data",
+ query: {
+ "populate[tags][fields][0]": "title",
+ status: "published",
+ },
+});
+---
+
+
+```
+
+## Important Notes
+
+### Do NOT:
+- Add components that don't have matching CMS types
+- Use inline styles (prefer Tailwind)
+- Create dependencies on specific apps
+- Hardcode environment-specific values
+- Skip type definitions
+
+### DO:
+- Follow existing naming conventions
+- Maintain type safety
+- Use Tailwind utility classes
+- Keep components reusable and generic
+- Document complex component logic
+- Test in multiple contexts
+- Ensure accessibility
+- Export all new components from index.ts
+- Use shared utilities from radarUtils.ts for radar components
+- Use `initializeOnReady` for client-side initialization in Astro components
+- **For radar animations**: Always use CSS keyframes (not transitions) and ensure animations for one responsive state don't affect other states
diff --git a/libs/ui/index.ts b/libs/ui/index.ts
index 8bb5790..7b4847b 100644
--- a/libs/ui/index.ts
+++ b/libs/ui/index.ts
@@ -19,6 +19,11 @@ import Listing from "./src/Listing.astro";
import SubtitleWithText from "./src/SubtitleWithText.astro";
import Quote from "./src/Quote.astro";
import Blogs from "./src/Blogs.astro";
+import TechnologyRadar from "./src/radar/TechnologyRadar.astro";
+import RadarQuadrant from "./src/radar/RadarQuadrant.astro";
+import RadarChart from "./src/radar/RadarChart.astro";
+import RadarQuadrantItemList from "./src/radar/RadarQuadrantItemList.astro";
+import { QUADRANTS, RINGS, initializeOnReady } from "./src/radar/radarUtils.js";
import ComponentRenderer from "./src/ComponentRenderer.astro";
@@ -44,5 +49,12 @@ export {
SubtitleWithText,
Quote,
Blogs,
+ TechnologyRadar,
+ RadarQuadrant,
+ RadarChart,
+ RadarQuadrantItemList,
+ QUADRANTS,
+ RINGS,
+ initializeOnReady,
ComponentRenderer
};
diff --git a/libs/ui/src/ComponentRenderer.astro b/libs/ui/src/ComponentRenderer.astro
index f440b91..81304ad 100644
--- a/libs/ui/src/ComponentRenderer.astro
+++ b/libs/ui/src/ComponentRenderer.astro
@@ -14,6 +14,7 @@ import Values from "./Values.astro";
import LogoCloud from "./LogoCloud.astro";
import Directors from "./Directors.astro";
import TeamCarousel from "./TeamCarousel.astro";
+import TechnologyRadar from "./radar/TechnologyRadar.astro";
interface Component {
__component: string;
@@ -42,6 +43,7 @@ const componentMap: Record = {
"ui.klant-logo-s": LogoCloud,
"ui.team": TeamCarousel,
"ui.directeuren": Directors,
+ "ui.technology-radar": TechnologyRadar,
};
---
diff --git a/libs/ui/src/Content.astro b/libs/ui/src/Content.astro
index bdf78ea..e0ae5ab 100644
--- a/libs/ui/src/Content.astro
+++ b/libs/ui/src/Content.astro
@@ -55,7 +55,7 @@ const site = import.meta.env.PUBLIC_IMAGES_URL;
-
+
diff --git a/libs/ui/src/Footer.astro b/libs/ui/src/Footer.astro
index 4bab0cc..2c019f2 100644
--- a/libs/ui/src/Footer.astro
+++ b/libs/ui/src/Footer.astro
@@ -25,7 +25,7 @@ const settings = globalSettings[0];
{settings.socials.map(s => (
s.isEnabled &&
{s.title}
-
+
))}
diff --git a/libs/ui/src/Page.astro b/libs/ui/src/Page.astro
index 8358330..a5e5ef5 100644
--- a/libs/ui/src/Page.astro
+++ b/libs/ui/src/Page.astro
@@ -65,7 +65,7 @@ const { page } = Astro.props as { page: Page };
-
+
diff --git a/libs/ui/src/SubtitleWithText.astro b/libs/ui/src/SubtitleWithText.astro
index 195ab69..fb6826c 100644
--- a/libs/ui/src/SubtitleWithText.astro
+++ b/libs/ui/src/SubtitleWithText.astro
@@ -9,11 +9,11 @@ interface SubtitleWithTextProps {
}
const { title, content } = Astro.props as SubtitleWithTextProps;
-const htmlContent = marked.parse(content);
+const htmlContent = await marked.parse(content);
---
{title}
-
+
diff --git a/libs/ui/src/Text.astro b/libs/ui/src/Text.astro
index 07c17f1..19e8776 100644
--- a/libs/ui/src/Text.astro
+++ b/libs/ui/src/Text.astro
@@ -8,6 +8,6 @@ interface RichTextProps {
}
const { content } = Astro.props as RichTextProps;
-const htmlContent = marked.parse(content);
+const htmlContent = await marked.parse(content);
---
-
+
diff --git a/libs/ui/src/radar/RadarChart.astro b/libs/ui/src/radar/RadarChart.astro
new file mode 100644
index 0000000..c132121
--- /dev/null
+++ b/libs/ui/src/radar/RadarChart.astro
@@ -0,0 +1,748 @@
+---
+import RadarQuadrant from "./RadarQuadrant.astro";
+import RadarQuadrantItemList from "./RadarQuadrantItemList.astro";
+import type { RadarItem, RadarItemWithNumber } from "@xprtz/cms";
+import { RINGS, QUADRANTS, type QuadrantName } from "./radarUtils";
+
+interface Props {
+ /**
+ * Array of radar items to display on the chart
+ */
+ items?: RadarItem[];
+
+ /**
+ * Size of each quadrant in pixels
+ */
+ quadrantSize?: number;
+
+ /**
+ * Slug of the parent page (for back links from radar items)
+ */
+ parentPageSlug?: string;
+}
+
+const { items = [], quadrantSize = 400, parentPageSlug } = Astro.props;
+
+// Margin between quadrants
+const margin = 20;
+
+// Add numbers to items and group by quadrant
+const itemsWithNumbers: RadarItemWithNumber[] = items.map((item, index) => ({
+ ...item,
+ number: index + 1,
+}));
+
+const itemsByQuadrant = Object.fromEntries(
+ Object.keys(QUADRANTS).map((quadrantName) => [
+ quadrantName,
+ itemsWithNumbers.filter((item) => item.quadrant === quadrantName),
+ ])
+) as Record
;
+
+// Helper maps for positioning
+const flexClassMap: Record = {
+ "top-left": "flex justify-end items-end",
+ "top-right": "flex justify-start items-end",
+ "bottom-left": "flex justify-end items-start",
+ "bottom-right": "flex justify-start items-start",
+};
+
+const labelPositionMap: Record = {
+ "top-left": { style: "top: 0; left: 0;", align: "text-left" },
+ "top-right": { style: "top: 0; right: 0;", align: "text-right" },
+ "bottom-left": { style: "bottom: 0; left: 0;", align: "text-left" },
+ "bottom-right": { style: "bottom: 0; right: 0;", align: "text-right" },
+};
+
+// Pre-calculate ring label positions and styles
+const ringLabelData = RINGS.map((ring, index) => {
+ const radius = quadrantSize * (ring.radiusPosition / 100);
+ const prevRadius =
+ index > 0 ? quadrantSize * (RINGS[index - 1].radiusPosition / 100) : 0;
+ const ringCenter = prevRadius + (radius - prevRadius) / 2;
+ // Calculate opacity: inner rings (Adopt) darker, outer rings (Hold) lighter
+ const opacity = 1 - index * 0.1;
+
+ return {
+ label: ring.label,
+ ringCenter,
+ opacity,
+ };
+});
+---
+
+
+
+
+
+
+ {
+ Object.entries(QUADRANTS).map(([name, config]) => {
+ const flexClasses = flexClassMap[config.gridPosition];
+ return (
+
+
+
+ );
+ })
+ }
+
+
+
+
+ {
+ ringLabelData.map((data) => {
+ const leftPosition = quadrantSize - data.ringCenter;
+ const labelStyle = `left: ${leftPosition}px; height: ${margin}px; opacity: ${data.opacity};`;
+
+ return (
+
+ {data.label}
+
+ );
+ })
+ }
+
+
+
+
+ {
+ ringLabelData.map((data) => {
+ const labelStyle = `left: ${data.ringCenter}px; height: ${margin}px; opacity: ${data.opacity};`;
+
+ return (
+
+ {data.label}
+
+ );
+ })
+ }
+
+
+
+ {
+ Object.entries(QUADRANTS).map(([name, config]) => {
+ const position = labelPositionMap[config.gridPosition];
+ const labelStyle = `${position.style} padding: 8px; color: ${config.color};`;
+
+ return (
+
+ {name}
+
+ );
+ })
+ }
+
+
+
+
+
+ {
+ Object.entries(QUADRANTS).map(([name, config]) => (
+
+ {name}
+
+ ))
+ }
+
+
+
+ {
+ Object.entries(QUADRANTS).map(([name, config]) => (
+
+ ))
+ }
+
+
+
+
+
diff --git a/libs/ui/src/radar/RadarQuadrant.astro b/libs/ui/src/radar/RadarQuadrant.astro
new file mode 100644
index 0000000..482a0dc
--- /dev/null
+++ b/libs/ui/src/radar/RadarQuadrant.astro
@@ -0,0 +1,246 @@
+---
+import type { RadarRing, RadarItem } from "@xprtz/cms";
+import { buildRadarItemLink, RINGS, QUADRANTS, type QuadrantName, type GridPosition } from "./radarUtils";
+
+interface ItemWithNumber extends RadarItem {
+ number: number;
+}
+
+interface Props {
+ /**
+ * Name of the quadrant
+ */
+ quadrantName: QuadrantName;
+
+ /**
+ * Size of the quadrant in pixels
+ */
+ size?: number;
+
+ /**
+ * Radar items to display in this quadrant
+ */
+ items?: ItemWithNumber[];
+
+ /**
+ * Slug of the parent page (for back links from radar items)
+ */
+ parentPageSlug?: string;
+}
+
+const { quadrantName, size = 400, items = [], parentPageSlug } = Astro.props;
+
+// Get configuration from QUADRANTS
+const { color, gridPosition } = QUADRANTS[quadrantName];
+
+// Map grid positions to SVG positions (internal mapping)
+// Grid position describes where the quadrant appears in the 2x2 grid
+// SVG position describes the angle range (0: 0-90°, 1: 90-180°, 2: 180-270°, 3: 270-360°)
+const gridToSvgPosition: Record = {
+ "top-right": 0, // 0-90°
+ "top-left": 1, // 90-180°
+ "bottom-left": 2, // 180-270°
+ "bottom-right": 3, // 270-360°
+};
+
+const position = gridToSvgPosition[gridPosition];
+
+// Determine origin point based on SVG position
+// Position 0 (0-90°): bottom-left corner
+// Position 1 (90-180°): bottom-right corner
+// Position 2 (180-270°): top-right corner
+// Position 3 (270-360°): top-left corner
+const origins = [
+ { x: 0, y: size }, // bottom-left
+ { x: size, y: size }, // bottom-right
+ { x: size, y: 0 }, // top-right
+ { x: 0, y: 0 }, // top-left
+];
+
+const origin = origins[position];
+
+// Generate SVG path for a quarter circle that fills the canvas
+const generateQuarterCirclePath = (radiusPercent: number): string => {
+ const radius = size * (radiusPercent / 100);
+
+ // Calculate end points based on position
+ // Position 0: arc from right (x+r, y) to top (x, y-r)
+ // Position 1: arc from left (x-r, y) to top (x, y-r)
+ // Position 2: arc from left (x-r, y) to bottom (x, y+r)
+ // Position 3: arc from right (x+r, y) to bottom (x, y+r)
+
+ const paths = [
+ // Position 0: bottom-left origin, arc goes right then up
+ `M ${origin.x} ${origin.y} L ${origin.x + radius} ${origin.y} A ${radius} ${radius} 0 0 0 ${origin.x} ${origin.y - radius} Z`,
+ // Position 1: bottom-right origin, arc goes left then up
+ `M ${origin.x} ${origin.y} L ${origin.x - radius} ${origin.y} A ${radius} ${radius} 0 0 1 ${origin.x} ${origin.y - radius} Z`,
+ // Position 2: top-right origin, arc goes left then down
+ `M ${origin.x} ${origin.y} L ${origin.x - radius} ${origin.y} A ${radius} ${radius} 0 0 0 ${origin.x} ${origin.y + radius} Z`,
+ // Position 3: top-left origin, arc goes right then down
+ `M ${origin.x} ${origin.y} L ${origin.x + radius} ${origin.y} A ${radius} ${radius} 0 0 1 ${origin.x} ${origin.y + radius} Z`,
+ ];
+
+ return paths[position];
+};
+
+// Circle radius for items (scaled for 20-30 items)
+const circleRadius = 12;
+// Maximum radius when hovering: radius 14 + stroke-width 3 = 17
+const maxCircleRadius = 17;
+
+// Function to get radius range for each ring
+const getRingRadiusRange = (ring: RadarRing): { min: number; max: number } => {
+ const ringData = RINGS.find((r) => r.label === ring);
+ if (!ringData) return { min: 0, max: size * 0.25 };
+
+ const index = RINGS.findIndex((r) => r.label === ring);
+ const minPercent = index > 0 ? RINGS[index - 1].radiusPosition : 0;
+ const maxPercent = ringData.radiusPosition;
+
+ return {
+ min: size * (minPercent / 100),
+ max: size * (maxPercent / 100),
+ };
+};
+
+// Position items within their rings using a simple grid-like distribution
+const positionedItems = items.map((item, index) => {
+ const { min, max } = getRingRadiusRange(item.ring);
+
+ // Use a pseudo-random but deterministic position based on item number
+ // This ensures consistent positioning across renders
+ const seed = item.number * 137.508; // Golden angle for better distribution
+ const angle = (seed % 90) * (Math.PI / 180); // Convert to radians within 90°
+
+ // Add some variation in radius within the ring
+ const radiusVariation = ((item.number * 73) % 100) / 100; // 0-1 range
+ const radius = min + (max - min) * (0.3 + radiusVariation * 0.6); // Use 30-90% of ring width
+
+ // Calculate position based on quadrant
+ let x: number, y: number;
+ if (position === 0) {
+ // Bottom-left origin: items spread in first quadrant (0-90°)
+ x = origin.x + radius * Math.cos(angle);
+ y = origin.y - radius * Math.sin(angle);
+ } else if (position === 1) {
+ // Bottom-right origin: items spread in second quadrant (90-180°)
+ x = origin.x - radius * Math.cos(angle);
+ y = origin.y - radius * Math.sin(angle);
+ } else if (position === 2) {
+ // Top-right origin: items spread in third quadrant (180-270°)
+ x = origin.x - radius * Math.cos(angle);
+ y = origin.y + radius * Math.sin(angle);
+ } else {
+ // Top-left origin: items spread in fourth quadrant (270-360°)
+ x = origin.x + radius * Math.cos(angle);
+ y = origin.y + radius * Math.sin(angle);
+ }
+
+ // Clamp positions to keep circles within SVG bounds (considering max radius with hover state)
+ x = Math.max(maxCircleRadius, Math.min(size - maxCircleRadius, x));
+ y = Math.max(maxCircleRadius, Math.min(size - maxCircleRadius, y));
+
+ return { ...item, x, y };
+});
+---
+
+
+
+ {
+ RINGS
+ .slice()
+ .reverse()
+ .map((ringData, index) => {
+ const actualIndex = RINGS.length - 1 - index;
+ // Calculate opacity based on ring position (inner rings lighter)
+ const opacity = 0.3 + actualIndex * 0.15;
+
+ return (
+
+
+
+ );
+ })
+ }
+
+
+ {
+ positionedItems.map((item) => {
+ const itemLink = buildRadarItemLink(item.slug, parentPageSlug);
+
+ // Serialize tags to JSON for data attribute
+ const tagsJson = JSON.stringify(item.tags?.map(t => t.title) || []);
+
+ return (
+
+
+
+ {item.number}. {item.title}
+
+
+
+ {item.number}
+
+
+
+ );
+ })
+ }
+
+
+
diff --git a/libs/ui/src/radar/RadarQuadrantItemList.astro b/libs/ui/src/radar/RadarQuadrantItemList.astro
new file mode 100644
index 0000000..cbe5ee7
--- /dev/null
+++ b/libs/ui/src/radar/RadarQuadrantItemList.astro
@@ -0,0 +1,234 @@
+---
+import type { RadarItemWithNumber } from "@xprtz/cms";
+import { buildRadarItemLink, RINGS } from "./radarUtils";
+
+interface Props {
+ /**
+ * Array of radar items for the selected quadrant
+ */
+ items: RadarItemWithNumber[];
+
+ /**
+ * Name of the quadrant
+ */
+ quadrantName: string;
+
+ /**
+ * Color for the quadrant
+ */
+ color: string;
+
+ /**
+ * Slug of the parent page (for back links from radar items)
+ */
+ parentPageSlug?: string;
+}
+
+const { items, quadrantName, color, parentPageSlug } = Astro.props;
+
+// Group items by ring
+const itemsByRing = RINGS.reduce(
+ (acc, { label }) => {
+ const ringItems = items.filter((item) => item.ring === label);
+ if (ringItems.length > 0) {
+ acc[label] = ringItems;
+ }
+ return acc;
+ },
+ {} as Record
+);
+---
+
+
+
+
+
+ {quadrantName}
+
+
+
+
+
+
+
+
+ Terug naar radar
+
+
+
+
+ {
+ Object.entries(itemsByRing).map(([ring, ringItems]) => (
+
+ ))
+ }
+
+
+
+
+
diff --git a/libs/ui/src/radar/TechnologyRadar.astro b/libs/ui/src/radar/TechnologyRadar.astro
new file mode 100644
index 0000000..6d74b2f
--- /dev/null
+++ b/libs/ui/src/radar/TechnologyRadar.astro
@@ -0,0 +1,128 @@
+---
+import { fetchData, type RadarItem } from "@xprtz/cms";
+import RadarChart from "./RadarChart.astro";
+
+interface TechnologyRadarProps {
+ __component: "ui.technology-radar";
+ title: string;
+}
+
+const { title } = Astro.props as TechnologyRadarProps;
+
+// Get the current page slug from Astro context
+// This will be used as the back link for radar items
+const currentPageSlug = Astro.url.pathname.replace(/^\//, "").replace(/\/$/, "") || "home";
+
+// Fetch all radar items for the chart
+const allRadarItems = await fetchData>({
+ endpoint: "radar-items",
+ wrappedByKey: "data",
+ query: {
+ "populate[tags][fields][0]": "title",
+ status: "published",
+ },
+});
+
+// Extract all unique tags from radar items
+const allTags = Array.from(
+ new Set(
+ allRadarItems.flatMap((item) => item.tags || []).map((tag) => tag.title)
+ )
+).sort();
+---
+
+
+ {title}
+
+
+
+
+{
+ allTags.length > 0 && (
+
+ {allTags.map((tag) => (
+
+ #{tag}
+
+ ))}
+
+ )
+}
+
+
diff --git a/libs/ui/src/radar/radarUtils.ts b/libs/ui/src/radar/radarUtils.ts
new file mode 100644
index 0000000..02d028b
--- /dev/null
+++ b/libs/ui/src/radar/radarUtils.ts
@@ -0,0 +1,72 @@
+import type { RadarRing } from "@xprtz/cms";
+
+/**
+ * Radar ring configuration with display order and radius positions
+ * Ordered from center (Adopt) to outside (Hold)
+ */
+export const RINGS: readonly { readonly label: RadarRing; readonly radiusPosition: number }[] = [
+ { label: "Adopt", radiusPosition: 25 },
+ { label: "Trial", radiusPosition: 50 },
+ { label: "Assess", radiusPosition: 75 },
+ { label: "Hold", radiusPosition: 100 },
+] as const;
+
+/**
+ * Grid position type for quadrants in the 2x2 grid layout
+ */
+export type GridPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
+
+/**
+ * Quadrant configuration with colors, keys, and grid positions
+ */
+export const QUADRANTS: Record = {
+ Tools: {
+ color: "#10b981", // green
+ key: "tools",
+ gridPosition: "top-left",
+ },
+ Technieken: {
+ color: "#3b82f6", // blue
+ key: "techniques",
+ gridPosition: "top-right",
+ },
+ Platformen: {
+ color: "#f59e0b", // amber
+ key: "platforms",
+ gridPosition: "bottom-left",
+ },
+ "Talen & Frameworks": {
+ color: "#ef4444", // red
+ key: "frameworks",
+ gridPosition: "bottom-right",
+ },
+};
+
+export type QuadrantName = keyof typeof QUADRANTS;
+
+/**
+ * Builds a link to a radar item page with optional back navigation
+ * @param slug - The slug of the radar item
+ * @param parentPageSlug - Optional slug of the parent page for back navigation
+ * @returns The full URL path to the radar item
+ */
+export function buildRadarItemLink(slug: string, parentPageSlug?: string): string {
+ return parentPageSlug
+ ? `/radar-items/${slug}?from=${encodeURIComponent(parentPageSlug)}`
+ : `/radar-items/${slug}`;
+}
+
+/**
+ * Initializes a callback function when the page is ready
+ * Handles both Astro page-load events and initial page load
+ * @param callback - The initialization function to execute
+ */
+export function initializeOnReady(callback: () => void): void {
+ document.addEventListener("astro:page-load", callback);
+ if (
+ document.readyState === "complete" ||
+ document.readyState === "interactive"
+ ) {
+ callback();
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
deleted file mode 100644
index afcbb16..0000000
--- a/pnpm-lock.yaml
+++ /dev/null
@@ -1,1028 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-importers:
-
- .:
- devDependencies:
- '@eslint/js':
- specifier: ^9.3.0
- version: 9.27.0
- eslint:
- specifier: ^9.3.0
- version: 9.27.0
- eslint-config-prettier:
- specifier: ^9.1.0
- version: 9.1.0(eslint@9.27.0)
- globals:
- specifier: ^15.3.0
- version: 15.15.0
- prettier:
- specifier: 3.2.5
- version: 3.2.5
- typescript:
- specifier: ^5.4.5
- version: 5.8.3
- typescript-eslint:
- specifier: ^8.0.0-alpha.20
- version: 8.32.1(eslint@9.27.0)(typescript@5.8.3)
-
-packages:
-
- '@eslint-community/eslint-utils@4.7.0':
- resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-
- '@eslint-community/regexpp@4.12.1':
- resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
- engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
- '@eslint/config-array@0.20.0':
- resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/config-helpers@0.2.2':
- resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/core@0.14.0':
- resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/eslintrc@3.3.1':
- resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/js@9.27.0':
- resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/object-schema@2.1.6':
- resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/plugin-kit@0.3.1':
- resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@humanfs/core@0.19.1':
- resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
- engines: {node: '>=18.18.0'}
-
- '@humanfs/node@0.16.6':
- resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
- engines: {node: '>=18.18.0'}
-
- '@humanwhocodes/module-importer@1.0.1':
- resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
- engines: {node: '>=12.22'}
-
- '@humanwhocodes/retry@0.3.1':
- resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
- engines: {node: '>=18.18'}
-
- '@humanwhocodes/retry@0.4.3':
- resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
- engines: {node: '>=18.18'}
-
- '@nodelib/fs.scandir@2.1.5':
- resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
- engines: {node: '>= 8'}
-
- '@nodelib/fs.stat@2.0.5':
- resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
- engines: {node: '>= 8'}
-
- '@nodelib/fs.walk@1.2.8':
- resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
- engines: {node: '>= 8'}
-
- '@types/estree@1.0.7':
- resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
-
- '@types/json-schema@7.0.15':
- resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
-
- '@typescript-eslint/eslint-plugin@8.32.1':
- resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <5.9.0'
-
- '@typescript-eslint/parser@8.32.1':
- resolution: {integrity: sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <5.9.0'
-
- '@typescript-eslint/scope-manager@8.32.1':
- resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/type-utils@8.32.1':
- resolution: {integrity: sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <5.9.0'
-
- '@typescript-eslint/types@8.32.1':
- resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/typescript-estree@8.32.1':
- resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <5.9.0'
-
- '@typescript-eslint/utils@8.32.1':
- resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <5.9.0'
-
- '@typescript-eslint/visitor-keys@8.32.1':
- resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- acorn-jsx@5.3.2:
- resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
- peerDependencies:
- acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
-
- acorn@8.14.1:
- resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
- engines: {node: '>=0.4.0'}
- hasBin: true
-
- ajv@6.12.6:
- resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
-
- ansi-styles@4.3.0:
- resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
- engines: {node: '>=8'}
-
- argparse@2.0.1:
- resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
-
- balanced-match@1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-
- brace-expansion@1.1.11:
- resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
-
- brace-expansion@2.0.1:
- resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
-
- braces@3.0.3:
- resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
- engines: {node: '>=8'}
-
- callsites@3.1.0:
- resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
- engines: {node: '>=6'}
-
- chalk@4.1.2:
- resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
- engines: {node: '>=10'}
-
- color-convert@2.0.1:
- resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
- engines: {node: '>=7.0.0'}
-
- color-name@1.1.4:
- resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
-
- concat-map@0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
-
- cross-spawn@7.0.6:
- resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
- engines: {node: '>= 8'}
-
- debug@4.4.1:
- resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
- engines: {node: '>=6.0'}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
-
- deep-is@0.1.4:
- resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
-
- escape-string-regexp@4.0.0:
- resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
- engines: {node: '>=10'}
-
- eslint-config-prettier@9.1.0:
- resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
- hasBin: true
- peerDependencies:
- eslint: '>=7.0.0'
-
- eslint-scope@8.3.0:
- resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- eslint-visitor-keys@3.4.3:
- resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
- eslint-visitor-keys@4.2.0:
- resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- eslint@9.27.0:
- resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- hasBin: true
- peerDependencies:
- jiti: '*'
- peerDependenciesMeta:
- jiti:
- optional: true
-
- espree@10.3.0:
- resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- esquery@1.6.0:
- resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
- engines: {node: '>=0.10'}
-
- esrecurse@4.3.0:
- resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
- engines: {node: '>=4.0'}
-
- estraverse@5.3.0:
- resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
- engines: {node: '>=4.0'}
-
- esutils@2.0.3:
- resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
- engines: {node: '>=0.10.0'}
-
- fast-deep-equal@3.1.3:
- resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
-
- fast-glob@3.3.3:
- resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
- engines: {node: '>=8.6.0'}
-
- fast-json-stable-stringify@2.1.0:
- resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
-
- fast-levenshtein@2.0.6:
- resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
-
- fastq@1.19.1:
- resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
-
- file-entry-cache@8.0.0:
- resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
- engines: {node: '>=16.0.0'}
-
- fill-range@7.1.1:
- resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
- engines: {node: '>=8'}
-
- find-up@5.0.0:
- resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
- engines: {node: '>=10'}
-
- flat-cache@4.0.1:
- resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
- engines: {node: '>=16'}
-
- flatted@3.3.3:
- resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
-
- glob-parent@5.1.2:
- resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
- engines: {node: '>= 6'}
-
- glob-parent@6.0.2:
- resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
- engines: {node: '>=10.13.0'}
-
- globals@14.0.0:
- resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
- engines: {node: '>=18'}
-
- globals@15.15.0:
- resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
- engines: {node: '>=18'}
-
- graphemer@1.4.0:
- resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
-
- has-flag@4.0.0:
- resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
- engines: {node: '>=8'}
-
- ignore@5.3.2:
- resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
- engines: {node: '>= 4'}
-
- ignore@7.0.4:
- resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==}
- engines: {node: '>= 4'}
-
- import-fresh@3.3.1:
- resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
- engines: {node: '>=6'}
-
- imurmurhash@0.1.4:
- resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
- engines: {node: '>=0.8.19'}
-
- is-extglob@2.1.1:
- resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
- engines: {node: '>=0.10.0'}
-
- is-glob@4.0.3:
- resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
- engines: {node: '>=0.10.0'}
-
- is-number@7.0.0:
- resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
- engines: {node: '>=0.12.0'}
-
- isexe@2.0.0:
- resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
-
- js-yaml@4.1.0:
- resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
- hasBin: true
-
- json-buffer@3.0.1:
- resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
-
- json-schema-traverse@0.4.1:
- resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
-
- json-stable-stringify-without-jsonify@1.0.1:
- resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
-
- keyv@4.5.4:
- resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
-
- levn@0.4.1:
- resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
- engines: {node: '>= 0.8.0'}
-
- locate-path@6.0.0:
- resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
- engines: {node: '>=10'}
-
- lodash.merge@4.6.2:
- resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
-
- merge2@1.4.1:
- resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
- engines: {node: '>= 8'}
-
- micromatch@4.0.8:
- resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
- engines: {node: '>=8.6'}
-
- minimatch@3.1.2:
- resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
-
- minimatch@9.0.5:
- resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
- engines: {node: '>=16 || 14 >=14.17'}
-
- ms@2.1.3:
- resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
-
- natural-compare@1.4.0:
- resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
-
- optionator@0.9.4:
- resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
- engines: {node: '>= 0.8.0'}
-
- p-limit@3.1.0:
- resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
- engines: {node: '>=10'}
-
- p-locate@5.0.0:
- resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
- engines: {node: '>=10'}
-
- parent-module@1.0.1:
- resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
- engines: {node: '>=6'}
-
- path-exists@4.0.0:
- resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
- engines: {node: '>=8'}
-
- path-key@3.1.1:
- resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
- engines: {node: '>=8'}
-
- picomatch@2.3.1:
- resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
- engines: {node: '>=8.6'}
-
- prelude-ls@1.2.1:
- resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
- engines: {node: '>= 0.8.0'}
-
- prettier@3.2.5:
- resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
- engines: {node: '>=14'}
- hasBin: true
-
- punycode@2.3.1:
- resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
- engines: {node: '>=6'}
-
- queue-microtask@1.2.3:
- resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
-
- resolve-from@4.0.0:
- resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
- engines: {node: '>=4'}
-
- reusify@1.1.0:
- resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
- engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
-
- run-parallel@1.2.0:
- resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
-
- semver@7.7.2:
- resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
- engines: {node: '>=10'}
- hasBin: true
-
- shebang-command@2.0.0:
- resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
- engines: {node: '>=8'}
-
- shebang-regex@3.0.0:
- resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
- engines: {node: '>=8'}
-
- strip-json-comments@3.1.1:
- resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
- engines: {node: '>=8'}
-
- supports-color@7.2.0:
- resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
- engines: {node: '>=8'}
-
- to-regex-range@5.0.1:
- resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
- engines: {node: '>=8.0'}
-
- ts-api-utils@2.1.0:
- resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
- engines: {node: '>=18.12'}
- peerDependencies:
- typescript: '>=4.8.4'
-
- type-check@0.4.0:
- resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
- engines: {node: '>= 0.8.0'}
-
- typescript-eslint@8.32.1:
- resolution: {integrity: sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <5.9.0'
-
- typescript@5.8.3:
- resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
- engines: {node: '>=14.17'}
- hasBin: true
-
- uri-js@4.4.1:
- resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
-
- which@2.0.2:
- resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
- engines: {node: '>= 8'}
- hasBin: true
-
- word-wrap@1.2.5:
- resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
- engines: {node: '>=0.10.0'}
-
- yocto-queue@0.1.0:
- resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
- engines: {node: '>=10'}
-
-snapshots:
-
- '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0)':
- dependencies:
- eslint: 9.27.0
- eslint-visitor-keys: 3.4.3
-
- '@eslint-community/regexpp@4.12.1': {}
-
- '@eslint/config-array@0.20.0':
- dependencies:
- '@eslint/object-schema': 2.1.6
- debug: 4.4.1
- minimatch: 3.1.2
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/config-helpers@0.2.2': {}
-
- '@eslint/core@0.14.0':
- dependencies:
- '@types/json-schema': 7.0.15
-
- '@eslint/eslintrc@3.3.1':
- dependencies:
- ajv: 6.12.6
- debug: 4.4.1
- espree: 10.3.0
- globals: 14.0.0
- ignore: 5.3.2
- import-fresh: 3.3.1
- js-yaml: 4.1.0
- minimatch: 3.1.2
- strip-json-comments: 3.1.1
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/js@9.27.0': {}
-
- '@eslint/object-schema@2.1.6': {}
-
- '@eslint/plugin-kit@0.3.1':
- dependencies:
- '@eslint/core': 0.14.0
- levn: 0.4.1
-
- '@humanfs/core@0.19.1': {}
-
- '@humanfs/node@0.16.6':
- dependencies:
- '@humanfs/core': 0.19.1
- '@humanwhocodes/retry': 0.3.1
-
- '@humanwhocodes/module-importer@1.0.1': {}
-
- '@humanwhocodes/retry@0.3.1': {}
-
- '@humanwhocodes/retry@0.4.3': {}
-
- '@nodelib/fs.scandir@2.1.5':
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- run-parallel: 1.2.0
-
- '@nodelib/fs.stat@2.0.5': {}
-
- '@nodelib/fs.walk@1.2.8':
- dependencies:
- '@nodelib/fs.scandir': 2.1.5
- fastq: 1.19.1
-
- '@types/estree@1.0.7': {}
-
- '@types/json-schema@7.0.15': {}
-
- '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3))(eslint@9.27.0)(typescript@5.8.3)':
- dependencies:
- '@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
- '@typescript-eslint/scope-manager': 8.32.1
- '@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
- '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
- '@typescript-eslint/visitor-keys': 8.32.1
- eslint: 9.27.0
- graphemer: 1.4.0
- ignore: 7.0.4
- natural-compare: 1.4.0
- ts-api-utils: 2.1.0(typescript@5.8.3)
- typescript: 5.8.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3)':
- dependencies:
- '@typescript-eslint/scope-manager': 8.32.1
- '@typescript-eslint/types': 8.32.1
- '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
- '@typescript-eslint/visitor-keys': 8.32.1
- debug: 4.4.1
- eslint: 9.27.0
- typescript: 5.8.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/scope-manager@8.32.1':
- dependencies:
- '@typescript-eslint/types': 8.32.1
- '@typescript-eslint/visitor-keys': 8.32.1
-
- '@typescript-eslint/type-utils@8.32.1(eslint@9.27.0)(typescript@5.8.3)':
- dependencies:
- '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
- '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
- debug: 4.4.1
- eslint: 9.27.0
- ts-api-utils: 2.1.0(typescript@5.8.3)
- typescript: 5.8.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/types@8.32.1': {}
-
- '@typescript-eslint/typescript-estree@8.32.1(typescript@5.8.3)':
- dependencies:
- '@typescript-eslint/types': 8.32.1
- '@typescript-eslint/visitor-keys': 8.32.1
- debug: 4.4.1
- fast-glob: 3.3.3
- is-glob: 4.0.3
- minimatch: 9.0.5
- semver: 7.7.2
- ts-api-utils: 2.1.0(typescript@5.8.3)
- typescript: 5.8.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/utils@8.32.1(eslint@9.27.0)(typescript@5.8.3)':
- dependencies:
- '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0)
- '@typescript-eslint/scope-manager': 8.32.1
- '@typescript-eslint/types': 8.32.1
- '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
- eslint: 9.27.0
- typescript: 5.8.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/visitor-keys@8.32.1':
- dependencies:
- '@typescript-eslint/types': 8.32.1
- eslint-visitor-keys: 4.2.0
-
- acorn-jsx@5.3.2(acorn@8.14.1):
- dependencies:
- acorn: 8.14.1
-
- acorn@8.14.1: {}
-
- ajv@6.12.6:
- dependencies:
- fast-deep-equal: 3.1.3
- fast-json-stable-stringify: 2.1.0
- json-schema-traverse: 0.4.1
- uri-js: 4.4.1
-
- ansi-styles@4.3.0:
- dependencies:
- color-convert: 2.0.1
-
- argparse@2.0.1: {}
-
- balanced-match@1.0.2: {}
-
- brace-expansion@1.1.11:
- dependencies:
- balanced-match: 1.0.2
- concat-map: 0.0.1
-
- brace-expansion@2.0.1:
- dependencies:
- balanced-match: 1.0.2
-
- braces@3.0.3:
- dependencies:
- fill-range: 7.1.1
-
- callsites@3.1.0: {}
-
- chalk@4.1.2:
- dependencies:
- ansi-styles: 4.3.0
- supports-color: 7.2.0
-
- color-convert@2.0.1:
- dependencies:
- color-name: 1.1.4
-
- color-name@1.1.4: {}
-
- concat-map@0.0.1: {}
-
- cross-spawn@7.0.6:
- dependencies:
- path-key: 3.1.1
- shebang-command: 2.0.0
- which: 2.0.2
-
- debug@4.4.1:
- dependencies:
- ms: 2.1.3
-
- deep-is@0.1.4: {}
-
- escape-string-regexp@4.0.0: {}
-
- eslint-config-prettier@9.1.0(eslint@9.27.0):
- dependencies:
- eslint: 9.27.0
-
- eslint-scope@8.3.0:
- dependencies:
- esrecurse: 4.3.0
- estraverse: 5.3.0
-
- eslint-visitor-keys@3.4.3: {}
-
- eslint-visitor-keys@4.2.0: {}
-
- eslint@9.27.0:
- dependencies:
- '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0)
- '@eslint-community/regexpp': 4.12.1
- '@eslint/config-array': 0.20.0
- '@eslint/config-helpers': 0.2.2
- '@eslint/core': 0.14.0
- '@eslint/eslintrc': 3.3.1
- '@eslint/js': 9.27.0
- '@eslint/plugin-kit': 0.3.1
- '@humanfs/node': 0.16.6
- '@humanwhocodes/module-importer': 1.0.1
- '@humanwhocodes/retry': 0.4.3
- '@types/estree': 1.0.7
- '@types/json-schema': 7.0.15
- ajv: 6.12.6
- chalk: 4.1.2
- cross-spawn: 7.0.6
- debug: 4.4.1
- escape-string-regexp: 4.0.0
- eslint-scope: 8.3.0
- eslint-visitor-keys: 4.2.0
- espree: 10.3.0
- esquery: 1.6.0
- esutils: 2.0.3
- fast-deep-equal: 3.1.3
- file-entry-cache: 8.0.0
- find-up: 5.0.0
- glob-parent: 6.0.2
- ignore: 5.3.2
- imurmurhash: 0.1.4
- is-glob: 4.0.3
- json-stable-stringify-without-jsonify: 1.0.1
- lodash.merge: 4.6.2
- minimatch: 3.1.2
- natural-compare: 1.4.0
- optionator: 0.9.4
- transitivePeerDependencies:
- - supports-color
-
- espree@10.3.0:
- dependencies:
- acorn: 8.14.1
- acorn-jsx: 5.3.2(acorn@8.14.1)
- eslint-visitor-keys: 4.2.0
-
- esquery@1.6.0:
- dependencies:
- estraverse: 5.3.0
-
- esrecurse@4.3.0:
- dependencies:
- estraverse: 5.3.0
-
- estraverse@5.3.0: {}
-
- esutils@2.0.3: {}
-
- fast-deep-equal@3.1.3: {}
-
- fast-glob@3.3.3:
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- '@nodelib/fs.walk': 1.2.8
- glob-parent: 5.1.2
- merge2: 1.4.1
- micromatch: 4.0.8
-
- fast-json-stable-stringify@2.1.0: {}
-
- fast-levenshtein@2.0.6: {}
-
- fastq@1.19.1:
- dependencies:
- reusify: 1.1.0
-
- file-entry-cache@8.0.0:
- dependencies:
- flat-cache: 4.0.1
-
- fill-range@7.1.1:
- dependencies:
- to-regex-range: 5.0.1
-
- find-up@5.0.0:
- dependencies:
- locate-path: 6.0.0
- path-exists: 4.0.0
-
- flat-cache@4.0.1:
- dependencies:
- flatted: 3.3.3
- keyv: 4.5.4
-
- flatted@3.3.3: {}
-
- glob-parent@5.1.2:
- dependencies:
- is-glob: 4.0.3
-
- glob-parent@6.0.2:
- dependencies:
- is-glob: 4.0.3
-
- globals@14.0.0: {}
-
- globals@15.15.0: {}
-
- graphemer@1.4.0: {}
-
- has-flag@4.0.0: {}
-
- ignore@5.3.2: {}
-
- ignore@7.0.4: {}
-
- import-fresh@3.3.1:
- dependencies:
- parent-module: 1.0.1
- resolve-from: 4.0.0
-
- imurmurhash@0.1.4: {}
-
- is-extglob@2.1.1: {}
-
- is-glob@4.0.3:
- dependencies:
- is-extglob: 2.1.1
-
- is-number@7.0.0: {}
-
- isexe@2.0.0: {}
-
- js-yaml@4.1.0:
- dependencies:
- argparse: 2.0.1
-
- json-buffer@3.0.1: {}
-
- json-schema-traverse@0.4.1: {}
-
- json-stable-stringify-without-jsonify@1.0.1: {}
-
- keyv@4.5.4:
- dependencies:
- json-buffer: 3.0.1
-
- levn@0.4.1:
- dependencies:
- prelude-ls: 1.2.1
- type-check: 0.4.0
-
- locate-path@6.0.0:
- dependencies:
- p-locate: 5.0.0
-
- lodash.merge@4.6.2: {}
-
- merge2@1.4.1: {}
-
- micromatch@4.0.8:
- dependencies:
- braces: 3.0.3
- picomatch: 2.3.1
-
- minimatch@3.1.2:
- dependencies:
- brace-expansion: 1.1.11
-
- minimatch@9.0.5:
- dependencies:
- brace-expansion: 2.0.1
-
- ms@2.1.3: {}
-
- natural-compare@1.4.0: {}
-
- optionator@0.9.4:
- dependencies:
- deep-is: 0.1.4
- fast-levenshtein: 2.0.6
- levn: 0.4.1
- prelude-ls: 1.2.1
- type-check: 0.4.0
- word-wrap: 1.2.5
-
- p-limit@3.1.0:
- dependencies:
- yocto-queue: 0.1.0
-
- p-locate@5.0.0:
- dependencies:
- p-limit: 3.1.0
-
- parent-module@1.0.1:
- dependencies:
- callsites: 3.1.0
-
- path-exists@4.0.0: {}
-
- path-key@3.1.1: {}
-
- picomatch@2.3.1: {}
-
- prelude-ls@1.2.1: {}
-
- prettier@3.2.5: {}
-
- punycode@2.3.1: {}
-
- queue-microtask@1.2.3: {}
-
- resolve-from@4.0.0: {}
-
- reusify@1.1.0: {}
-
- run-parallel@1.2.0:
- dependencies:
- queue-microtask: 1.2.3
-
- semver@7.7.2: {}
-
- shebang-command@2.0.0:
- dependencies:
- shebang-regex: 3.0.0
-
- shebang-regex@3.0.0: {}
-
- strip-json-comments@3.1.1: {}
-
- supports-color@7.2.0:
- dependencies:
- has-flag: 4.0.0
-
- to-regex-range@5.0.1:
- dependencies:
- is-number: 7.0.0
-
- ts-api-utils@2.1.0(typescript@5.8.3):
- dependencies:
- typescript: 5.8.3
-
- type-check@0.4.0:
- dependencies:
- prelude-ls: 1.2.1
-
- typescript-eslint@8.32.1(eslint@9.27.0)(typescript@5.8.3):
- dependencies:
- '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3))(eslint@9.27.0)(typescript@5.8.3)
- '@typescript-eslint/parser': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
- '@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
- eslint: 9.27.0
- typescript: 5.8.3
- transitivePeerDependencies:
- - supports-color
-
- typescript@5.8.3: {}
-
- uri-js@4.4.1:
- dependencies:
- punycode: 2.3.1
-
- which@2.0.2:
- dependencies:
- isexe: 2.0.0
-
- word-wrap@1.2.5: {}
-
- yocto-queue@0.1.0: {}