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.avatar.alternateText} +
+

{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", + }, + }); + --- + + + + +
+

Related Articles

+ +
+
+ ``` + +## 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; +{hero.images[0].alternateText} +``` + +### 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} - + {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]) => ( + + )) + } +
+ + + + 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} +

+
+ +
+ +
+ +
+ { + Object.entries(itemsByRing).map(([ring, ringItems]) => ( +
+

+ {ring} +

+ +
+ )) + } +
+
+
+ + 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) => ( + + ))} +
+ ) +} + + 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: {}