From b65a61330d158179c997827887b65b89e023ca4a Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Fri, 19 Dec 2025 09:41:59 +0100 Subject: [PATCH 01/44] added AGENTS.md --- AGENTS.md | 187 ++++++++ DEVELOPMENT_GUIDE.md | 451 ++++++++++++++++++ apps/dotnet/AGENTS.md | 528 +++++++++++++++++++++ libs/cms/AGENTS.md | 648 ++++++++++++++++++++++++++ libs/ui/AGENTS.md | 379 +++++++++++++++ pnpm-lock.yaml | 1028 ----------------------------------------- 6 files changed, 2193 insertions(+), 1028 deletions(-) create mode 100644 AGENTS.md create mode 100644 DEVELOPMENT_GUIDE.md create mode 100644 apps/dotnet/AGENTS.md create mode 100644 libs/cms/AGENTS.md create mode 100644 libs/ui/AGENTS.md delete mode 100644 pnpm-lock.yaml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..57b6282 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,187 @@ +# 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: `main` +- Recent commits focus on Astro upgrades and feature additions +- Deleted `pnpm-lock.yaml` (using npm instead of pnpm) + +## 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, +}; +``` + +### 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 + +``` + +## 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 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: {} From 11cdbc050fa4c7af641693e47826369374140d99 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Fri, 19 Dec 2025 09:49:28 +0100 Subject: [PATCH 02/44] added radar item model --- libs/cms/index.ts | 6 +++++- libs/cms/models/radarItem.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 libs/cms/models/radarItem.ts diff --git a/libs/cms/index.ts b/libs/cms/index.ts index b546dc1..1f8b7c9 100644 --- a/libs/cms/index.ts +++ b/libs/cms/index.ts @@ -17,6 +17,7 @@ import { Team } from "./models/team.js"; import { Directors } from "./models/directors.js"; import { Social } from "./models/social.js"; import { AlgemeneVoorwaarden } from "./models/algemeneVoorwaarden.js"; +import { RadarItem, RadarQuadrant, RadarRing } from "./models/radarItem.js"; import { HomePage } from "./models/homePage.js"; @@ -43,6 +44,9 @@ export { type Social, type AlgemeneVoorwaarden, type Directors, - type HomePage + type HomePage, + type RadarItem, + type RadarQuadrant, + type RadarRing }; diff --git a/libs/cms/models/radarItem.ts b/libs/cms/models/radarItem.ts new file mode 100644 index 0000000..d047aa9 --- /dev/null +++ b/libs/cms/models/radarItem.ts @@ -0,0 +1,17 @@ +import { ListItem } from "./listItem.js"; +import { Tag } from "./tag.js"; + +export type RadarQuadrant = "Techniques" | "Tools" | "Platforms" | "Languages & Frameworks"; +export type RadarRing = "Adopt" | "Trial" | "Assess" | "Hold"; + +export type RadarItem = { + slug: string; + quadrant: RadarQuadrant; + ring: RadarRing; + title: string; + description: string; + pros: ListItem[]; + cons: ListItem[]; + conclusion: string; + tags: Tag[]; +} From dc00baae420aac6b61339d2395ce8f4f06276e54 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Fri, 19 Dec 2025 10:27:28 +0100 Subject: [PATCH 03/44] added placeholder pages for radar --- apps/dotnet/RADAR_SETUP.md | 230 ++++++++++++++++++ apps/dotnet/src/pages/[slug].astro | 6 +- .../pages/technology-radar/[radarItem].astro | 104 ++++++++ .../pages/technology-radar/page/[page].astro | 124 ++++++++++ libs/ui/index.ts | 2 + libs/ui/src/RadarItems.astro | 98 ++++++++ 6 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 apps/dotnet/RADAR_SETUP.md create mode 100644 apps/dotnet/src/pages/technology-radar/[radarItem].astro create mode 100644 apps/dotnet/src/pages/technology-radar/page/[page].astro create mode 100644 libs/ui/src/RadarItems.astro diff --git a/apps/dotnet/RADAR_SETUP.md b/apps/dotnet/RADAR_SETUP.md new file mode 100644 index 0000000..bbaa93a --- /dev/null +++ b/apps/dotnet/RADAR_SETUP.md @@ -0,0 +1,230 @@ +# Technology Radar Setup Guide + +This guide explains how to set up the Technology Radar feature in your Strapi CMS. + +## Overview + +The Technology Radar is now set up to work similarly to the articles (artikelen) feature: +- Main listing page with pagination at: `/technology-radar/page/1` +- Individual item pages at: `/technology-radar/{item-slug}` +- Integration with the existing "Technology Radar" page (slug: `technology-radar`) + +## Files Created + +### CMS Model +- **[libs/cms/models/radarItem.ts](../../libs/cms/models/radarItem.ts)** - TypeScript type definition +- **Updated [libs/cms/index.ts](../../libs/cms/index.ts)** - Exports RadarItem, RadarQuadrant, and RadarRing types + +### UI Components +- **[libs/ui/src/RadarItems.astro](../../libs/ui/src/RadarItems.astro)** - Listing component with pagination +- **Updated [libs/ui/index.ts](../../libs/ui/index.ts)** - Exports RadarItems component + +### App Pages +- **[src/pages/technology-radar/[radarItem].astro](src/pages/technology-radar/[radarItem].astro)** - Individual radar item pages +- **[src/pages/technology-radar/page/[page].astro](src/pages/technology-radar/page/[page].astro)** - Paginated listing +- **Updated [src/pages/[slug].astro](src/pages/[slug].astro)** - Added rewrite rule to redirect `/technology-radar` to `/technology-radar/page/1` + +## Strapi CMS Configuration + +### 1. Create Collection Type: "Radar Items" + +In Strapi Admin Panel: +1. Go to **Content-Type Builder** +2. Click **Create new collection type** +3. Name: `radar-item` (singular), `radar-items` (plural) + +### 2. Add Fields + +Add the following fields to the `radar-items` collection: + +#### Basic Fields +- **slug** (Text - Short text) + - Type: Text + - Required: Yes + - Unique: Yes + +- **title** (Text - Short text) + - Type: Text + - Required: Yes + +- **description** (Text - Long text) + - Type: Text + - Required: Yes + +- **conclusion** (Text - Long text) + - Type: Text + - Required: No + +#### Enumeration Fields +- **quadrant** (Enumeration) + - Type: Enumeration + - Values: + - `Techniques` + - `Tools` + - `Platforms` + - `Languages & Frameworks` + - Required: Yes + +- **ring** (Enumeration) + - Type: Enumeration + - Values: + - `Adopt` + - `Trial` + - `Assess` + - `Hold` + - Required: Yes + +#### Component Fields (Repeatable) +- **pros** (Component - Repeatable) + - Component: `shared.list-item` (reuse existing ListItem component) + - Required: No + +- **cons** (Component - Repeatable) + - Component: `shared.list-item` (reuse existing ListItem component) + - Required: No + +#### Relation Fields +- **tags** (Relation - Many to Many) + - Relation type: Relation with tags + - Many to Many relationship + - Required: No + +#### System Fields +- **status** (Enumeration) + - Type: Enumeration + - Values: + - `published` + - `draft` + - Default value: `draft` + +### 3. Configure API Access + +1. Go to **Settings → Roles → Public** +2. Enable **find** and **findOne** permissions for `radar-items` +3. Save + +### 4. Verify Existing Page + +Make sure the page with slug `technology-radar` exists: +1. Go to **Content Manager → Pages** +2. Find or create page with: + - **Slug**: `technology-radar` + - **Title (Website)**: Technology Radar (or your preferred title) + - **Title (CMS)**: Technology Radar + - **Description**: Your radar description + - **Tagline**: e.g., "XPRTZ" + - **Site**: `dotnet` + - **Status**: `published` + +## Creating Content + +### Example Radar Item + +Here's an example of a complete radar item entry: + +**Title**: Minimal APIs in .NET +**Slug**: minimal-apis-dotnet +**Quadrant**: Tools +**Ring**: Adopt +**Description**: Minimal APIs provide a simplified approach to building HTTP APIs with ASP.NET Core. They reduce the ceremony and boilerplate traditionally associated with creating APIs. +**Status**: published + +**Pros**: +1. **Less Boilerplate** - Significantly reduces the amount of code needed to create endpoints +2. **Performance** - Lightweight and fast, with minimal overhead +3. **Easy to Learn** - Great for developers new to ASP.NET Core + +**Cons**: +1. **Limited Conventions** - Less structure than controllers, can lead to inconsistent APIs +2. **Complex Routing** - More difficult to manage in large applications +3. **Testing** - Slightly more challenging to test compared to controller-based APIs + +**Conclusion**: Minimal APIs are excellent for microservices and small to medium-sized APIs. For larger applications with complex requirements, traditional controllers may still be preferable. + +**Tags**: ASP.NET Core, APIs, .NET 6 + +## URL Structure + +Once content is created, the following URLs will be available: + +- **Main listing**: `/technology-radar` (automatically redirects to `/technology-radar/page/1`) +- **Main listing (page 1)**: `/technology-radar/page/1` +- **Main listing (page 2)**: `/technology-radar/page/2` +- **Individual item**: `/technology-radar/minimal-apis-dotnet` + +Note: The `/technology-radar` URL is configured in [src/pages/[slug].astro](src/pages/[slug].astro) to rewrite to the paginated listing at `/technology-radar/page/1`, similar to how `/artikelen` redirects to `/artikelen/page/1`. + +## Features + +### Listing Page Features +- Displays radar item title, description, quadrant, ring, and tags +- Color-coded ring badges: + - **Adopt**: Green + - **Trial**: Blue + - **Assess**: Yellow + - **Hold**: Red +- Pagination (10 items per page) +- Links to individual item pages + +### Individual Item Page Features +- Full title and description +- Quadrant and ring display +- **Pros section** with green checkmark icons +- **Cons section** with red X icons +- **Conclusion section** +- Tags display +- Back link to listing + +## API Queries Used + +### For Listing Page +```javascript +endpoint: "radar-items" +query: { + "populate[tags][fields][0]": "title", + status: "published", +} +``` + +### For Individual Item Page +```javascript +endpoint: "radar-items" +query: { + "populate[pros]": "*", + "populate[cons]": "*", + "populate[tags][fields][0]": "title", + status: "published", +} +``` + +## Testing + +1. Create at least one radar item in Strapi +2. Set **status** to `published` +3. Build the app: `npm run build` +4. Preview: `npm run preview` +5. Navigate to `/technology-radar/page/1` + +## Troubleshooting + +### No items showing +- Verify items are published (status: "published") +- Verify API permissions are enabled in Strapi + +### Build fails +- Ensure Strapi CMS is running and accessible +- Check environment variables are set correctly +- Verify the page with slug "technology-radar" exists + +### Styling issues +- Check that Tailwind CSS is configured properly +- Verify ring colors are defined in the component + +## Next Steps + +Consider adding these features: +1. Filter by quadrant or ring +2. Search functionality +3. Visual radar chart representation +4. Related items based on tags +5. Export to PDF functionality diff --git a/apps/dotnet/src/pages/[slug].astro b/apps/dotnet/src/pages/[slug].astro index 6f44bd0..f885fe8 100644 --- a/apps/dotnet/src/pages/[slug].astro +++ b/apps/dotnet/src/pages/[slug].astro @@ -6,11 +6,15 @@ import { Page } from "@xprtz/ui"; // Get the slug parameter from the URL const { slug } = Astro.params; -// Check if the slug is exactly 'artikelen' +// Check if the slug is exactly 'artikelen' or 'technology-radar' if (slug === 'artikelen') { return Astro.rewrite('/artikelen/page/1'); } +if (slug === 'technology-radar') { + return Astro.rewrite('/technology-radar/page/1'); +} + export async function getStaticPaths() { const pageData = await fetchData>({ endpoint: "pages", diff --git a/apps/dotnet/src/pages/technology-radar/[radarItem].astro b/apps/dotnet/src/pages/technology-radar/[radarItem].astro new file mode 100644 index 0000000..e693b80 --- /dev/null +++ b/apps/dotnet/src/pages/technology-radar/[radarItem].astro @@ -0,0 +1,104 @@ +--- +import Layout from "../../layouts/layout.astro"; +import { fetchData, type RadarItem } from "@xprtz/cms"; + +export async function getStaticPaths() { + const pageData = await fetchData>({ + endpoint: "radar-items", + wrappedByKey: "data", + query: { + "populate[pros]": "*", + "populate[cons]": "*", + "populate[tags][fields][0]": "title", + status: "published", + }, + }); + + // Return empty array if no items found to prevent build errors + if (!pageData || pageData.length === 0) { + return []; + } + + return pageData.map((data: RadarItem) => ({ + params: { radarItem: data.slug }, + props: data, + })); +} + +const radarItem: RadarItem = Astro.props; +--- + + +
+
+

Technology Radar

+

+ {radarItem.title} +

+

+ {radarItem.description} +

+ + {radarItem.tags && radarItem.tags.length > 0 && ( +
+ {radarItem.tags.map((tag) => ( + + #{tag.title} + + ))} +
+ )} + + {radarItem.pros && radarItem.pros.length > 0 && ( +
+

Voordelen

+
    + {radarItem.pros.map((pro) => ( +
  • + +
    + {pro.title}. {pro.description} +
    +
  • + ))} +
+
+ )} + + {radarItem.cons && radarItem.cons.length > 0 && ( +
+

Nadelen

+
    + {radarItem.cons.map((con) => ( +
  • + +
    + {con.title}. {con.description} +
    +
  • + ))} +
+
+ )} + + {radarItem.conclusion && ( +
+

Conclusie

+

+ {radarItem.conclusion} +

+
+ )} + + +
+
+
diff --git a/apps/dotnet/src/pages/technology-radar/page/[page].astro b/apps/dotnet/src/pages/technology-radar/page/[page].astro new file mode 100644 index 0000000..1620da2 --- /dev/null +++ b/apps/dotnet/src/pages/technology-radar/page/[page].astro @@ -0,0 +1,124 @@ +--- +import type { GetStaticPaths } from "astro"; +import { fetchData, type RadarItem, type Page as PageType } from "@xprtz/cms"; +import Layout from "../../../layouts/layout.astro"; +import { Container, RadarItems } from "@xprtz/ui"; + +interface PageData { + data: RadarItem[]; + start: number; + end: number; + size: number; + total: number; + currentPage: number; + lastPage: number; + url: Url; +} + +interface Url { + current: string; + next: string; + prev: string; + first: string; + last: string; +} + +export const getStaticPaths: GetStaticPaths = async ({ paginate }) => { + const radarItems = await fetchData>({ + endpoint: "radar-items", + wrappedByKey: "data", + query: { + "populate[tags][fields][0]": "title", + status: "published", + }, + }); + + // Return empty array if no items found to prevent build errors + if (!radarItems || radarItems.length === 0) { + return []; + } + + return paginate(radarItems, { pageSize: 10 }); +}; + +const site = import.meta.env.PUBLIC_SITE || "no-site-found"; +const radarPages = await fetchData>({ + endpoint: "pages", + wrappedByKey: "data", + query: { + "filters[site][$eq]": site, + "filters[slug][$eq]": "technology-radar", + status: "published", + }, +}); + +const { page } = Astro.props as { page: PageData }; + +const radarPage = radarPages[0]; +--- + + + + + + +
+
+
+

+ {radarPage.tagline} +

+

+ {radarPage.title_website} +

+

{radarPage.description}

+
+ +
+
+
+
+
+
diff --git a/libs/ui/index.ts b/libs/ui/index.ts index 8bb5790..c80f178 100644 --- a/libs/ui/index.ts +++ b/libs/ui/index.ts @@ -19,6 +19,7 @@ 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 RadarItems from "./src/RadarItems.astro"; import ComponentRenderer from "./src/ComponentRenderer.astro"; @@ -44,5 +45,6 @@ export { SubtitleWithText, Quote, Blogs, + RadarItems, ComponentRenderer }; diff --git a/libs/ui/src/RadarItems.astro b/libs/ui/src/RadarItems.astro new file mode 100644 index 0000000..92ca352 --- /dev/null +++ b/libs/ui/src/RadarItems.astro @@ -0,0 +1,98 @@ +--- +import { type RadarItem } from "@xprtz/cms"; + +interface PageData { + data: RadarItem[]; + start: number; + end: number; + size: number; + total: number; + currentPage: number; + lastPage: number; + url: Url; +} + +interface Url { + current: string; + next: string; + prev: string; + first: string; + last: string; +} + +const { page } = Astro.props as { page: PageData }; + +const ringColors = { + "Adopt": "bg-green-100 text-green-700", + "Trial": "bg-blue-100 text-blue-700", + "Assess": "bg-yellow-100 text-yellow-700", + "Hold": "bg-red-100 text-red-700", +}; +--- +
+
+ {page.data.map(item => ( +
+
+
+ + {item.ring} + + {item.quadrant} +
+
+

+ + + {item.title} + +

+

+ {item.description} +

+
+ {item.tags && item.tags.length > 0 && ( +
+ {item.tags.map((tag) => ( + + #{tag.title} + + ))} +
+ )} +
+
+ ))} +
+ +
From cabe5f51dfa6998dc05c92356bea0afa46c4df2c Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Fri, 19 Dec 2025 16:08:22 +0100 Subject: [PATCH 04/44] added radar chart and quadrant components --- .../pages/technology-radar/page/[page].astro | 56 ++--- libs/ui/AGENTS.md | 109 ++++++++++ libs/ui/index.ts | 6 +- libs/ui/src/RadarChart.astro | 99 +++++++++ libs/ui/src/RadarItems.astro | 98 --------- libs/ui/src/RadarQuadrant.astro | 191 ++++++++++++++++++ 6 files changed, 419 insertions(+), 140 deletions(-) create mode 100644 libs/ui/src/RadarChart.astro delete mode 100644 libs/ui/src/RadarItems.astro create mode 100644 libs/ui/src/RadarQuadrant.astro diff --git a/apps/dotnet/src/pages/technology-radar/page/[page].astro b/apps/dotnet/src/pages/technology-radar/page/[page].astro index 1620da2..f177aa5 100644 --- a/apps/dotnet/src/pages/technology-radar/page/[page].astro +++ b/apps/dotnet/src/pages/technology-radar/page/[page].astro @@ -2,43 +2,11 @@ import type { GetStaticPaths } from "astro"; import { fetchData, type RadarItem, type Page as PageType } from "@xprtz/cms"; import Layout from "../../../layouts/layout.astro"; -import { Container, RadarItems } from "@xprtz/ui"; +import { Container, RadarChart } from "@xprtz/ui"; -interface PageData { - data: RadarItem[]; - start: number; - end: number; - size: number; - total: number; - currentPage: number; - lastPage: number; - url: Url; -} - -interface Url { - current: string; - next: string; - prev: string; - first: string; - last: string; -} - -export const getStaticPaths: GetStaticPaths = async ({ paginate }) => { - const radarItems = await fetchData>({ - endpoint: "radar-items", - wrappedByKey: "data", - query: { - "populate[tags][fields][0]": "title", - status: "published", - }, - }); - - // Return empty array if no items found to prevent build errors - if (!radarItems || radarItems.length === 0) { - return []; - } - - return paginate(radarItems, { pageSize: 10 }); +export const getStaticPaths: GetStaticPaths = async () => { + // Generate a single page for the radar + return [{ params: { page: "1" } }]; }; const site = import.meta.env.PUBLIC_SITE || "no-site-found"; @@ -52,9 +20,17 @@ const radarPages = await fetchData>({ }, }); -const { page } = Astro.props as { page: PageData }; - const radarPage = radarPages[0]; + +// Fetch all radar items for the chart +const allRadarItems = await fetchData>({ + endpoint: "radar-items", + wrappedByKey: "data", + query: { + "populate[tags][fields][0]": "title", + status: "published", + }, +}); ---

{radarPage.description}

-
- +
+
diff --git a/libs/ui/AGENTS.md b/libs/ui/AGENTS.md index 6a4f6c5..ce3a01b 100644 --- a/libs/ui/AGENTS.md +++ b/libs/ui/AGENTS.md @@ -54,6 +54,10 @@ libs/ui/ - [BlogListing.astro](src/BlogListing.astro) - Blog posts grid/list - [BlogCard.astro](src/BlogCard.astro) - Individual blog post card +### Technology Radar Components +- [RadarChart.astro](src/RadarChart.astro) - Technology radar visualization with four quadrants +- [RadarQuadrant.astro](src/RadarQuadrant.astro) - Individual radar quadrant with concentric rings and items + ### Utility Components - [LogoCloud.astro](src/LogoCloud.astro) - Client/partner logo display - [ComponentRenderer.astro](src/ComponentRenderer.astro) - **Dynamic component routing** (maps CMS `__component` to actual components) @@ -359,6 +363,111 @@ Components like [TeamCarousel.astro](src/TeamCarousel.astro) use Embla Carousel: ``` +## Technology Radar Pattern + +The Technology Radar components provide an interactive visualization of technology adoption: + +### RadarChart Component + +The main radar visualization that combines four quadrants into a complete circle. + +**Props:** +- `items?: RadarItem[]` - Array of radar items from CMS +- `quadrantSize?: number` - Size of each quadrant in pixels (default: 400) + +**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 + +**Usage:** +```astro +import { RadarChart } from "@xprtz/ui"; + + +``` + +### RadarQuadrant Component + +Individual quadrant representing 90° of the radar (one quarter circle). + +**Props:** +- `position: 0 | 1 | 2 | 3` - Quadrant position (0: 0-90°, 1: 90-180°, 2: 180-270°, 3: 270-360°) +- `color: string` - Color for the quadrant +- `size?: number` - Size in pixels (default: 400) +- `items?: ItemWithNumber[]` - Radar items to display + +**Features:** +- Four concentric rings representing adoption stages: + - **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) +- Origin point positioned at appropriate corner based on position + +**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 + +**Usage:** +```astro +import { RadarQuadrant } from "@xprtz/ui"; + + +``` + +### RadarItem Type + +Items require the following structure: +```typescript +interface ItemWithNumber extends RadarItem { + number: number; // Sequential number (1, 2, 3...) + title: string; // Display title + slug: string; // URL slug for detail page + ring: RadarRing; // "Adopt" | "Trial" | "Assess" | "Hold" + quadrant: RadarQuadrant; // Quadrant name +} +``` + +### Integration Example + +```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: diff --git a/libs/ui/index.ts b/libs/ui/index.ts index c80f178..e294f2e 100644 --- a/libs/ui/index.ts +++ b/libs/ui/index.ts @@ -19,7 +19,8 @@ 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 RadarItems from "./src/RadarItems.astro"; +import RadarQuadrant from "./src/RadarQuadrant.astro"; +import RadarChart from "./src/RadarChart.astro"; import ComponentRenderer from "./src/ComponentRenderer.astro"; @@ -45,6 +46,7 @@ export { SubtitleWithText, Quote, Blogs, - RadarItems, + RadarQuadrant, + RadarChart, ComponentRenderer }; diff --git a/libs/ui/src/RadarChart.astro b/libs/ui/src/RadarChart.astro new file mode 100644 index 0000000..46f2b11 --- /dev/null +++ b/libs/ui/src/RadarChart.astro @@ -0,0 +1,99 @@ +--- +import RadarQuadrant from "./RadarQuadrant.astro"; +import type { RadarItem } from "@xprtz/cms"; + +interface Props { + /** + * Array of radar items to display on the chart + */ + items?: RadarItem[]; + + /** + * Size of each quadrant in pixels + */ + quadrantSize?: number; +} + +const { items = [], quadrantSize = 400 } = Astro.props; + +// Margin between quadrants +const margin = 20; + +// Define colors for each quadrant +const quadrantColors = { + "Techniques": "#3b82f6", // blue + "Tools": "#10b981", // green + "Platforms": "#f59e0b", // amber + "Languages & Frameworks": "#ef4444" // red +}; + +// Add numbers to items and group by quadrant +const itemsWithNumbers = items.map((item, index) => ({ + ...item, + number: index + 1 +})); + +const itemsByQuadrant = { + "Techniques": itemsWithNumbers.filter(item => item.quadrant === "Techniques"), + "Tools": itemsWithNumbers.filter(item => item.quadrant === "Tools"), + "Platforms": itemsWithNumbers.filter(item => item.quadrant === "Platforms"), + "Languages & Frameworks": itemsWithNumbers.filter(item => item.quadrant === "Languages & Frameworks") +}; + +// Calculate total size +const totalWidth = quadrantSize * 2 + margin; +const totalHeight = quadrantSize * 2 + margin; +--- + +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + diff --git a/libs/ui/src/RadarItems.astro b/libs/ui/src/RadarItems.astro deleted file mode 100644 index 92ca352..0000000 --- a/libs/ui/src/RadarItems.astro +++ /dev/null @@ -1,98 +0,0 @@ ---- -import { type RadarItem } from "@xprtz/cms"; - -interface PageData { - data: RadarItem[]; - start: number; - end: number; - size: number; - total: number; - currentPage: number; - lastPage: number; - url: Url; -} - -interface Url { - current: string; - next: string; - prev: string; - first: string; - last: string; -} - -const { page } = Astro.props as { page: PageData }; - -const ringColors = { - "Adopt": "bg-green-100 text-green-700", - "Trial": "bg-blue-100 text-blue-700", - "Assess": "bg-yellow-100 text-yellow-700", - "Hold": "bg-red-100 text-red-700", -}; ---- -
-
- {page.data.map(item => ( -
-
-
- - {item.ring} - - {item.quadrant} -
-
-

- - - {item.title} - -

-

- {item.description} -

-
- {item.tags && item.tags.length > 0 && ( -
- {item.tags.map((tag) => ( - - #{tag.title} - - ))} -
- )} -
-
- ))} -
- -
diff --git a/libs/ui/src/RadarQuadrant.astro b/libs/ui/src/RadarQuadrant.astro new file mode 100644 index 0000000..845aa05 --- /dev/null +++ b/libs/ui/src/RadarQuadrant.astro @@ -0,0 +1,191 @@ +--- +import type { RadarRing, RadarItem } from "@xprtz/cms"; + +interface ItemWithNumber extends RadarItem { + number: number; +} + +interface Props { + /** + * Which quadrant position (0: 0-90°, 1: 90-180°, 2: 180-270°, 3: 270-360°) + */ + position: 0 | 1 | 2 | 3; + + /** + * The color of the quadrant + */ + color: string; + + /** + * Size of the quadrant in pixels + */ + size?: number; + + /** + * Radar items to display in this quadrant + */ + items?: ItemWithNumber[]; +} + +const { position, color, size = 400, items = [] } = Astro.props; + +// Define the four rings from center to outside +const rings: { ring: RadarRing; radiusPercent: number }[] = [ + { ring: "Adopt", radiusPercent: 25 }, + { ring: "Trial", radiusPercent: 50 }, + { ring: "Assess", radiusPercent: 75 }, + { ring: "Hold", radiusPercent: 100 }, +]; + +// Determine origin point based on quadrant 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; + +// Function to get radius range for each ring +const getRingRadiusRange = (ring: RadarRing): { min: number; max: number } => { + const ringData = rings.find(r => r.ring === ring); + if (!ringData) return { min: 0, max: size * 0.25 }; + + const index = rings.findIndex(r => r.ring === ring); + const minPercent = index > 0 ? rings[index - 1].radiusPercent : 0; + const maxPercent = ringData.radiusPercent; + + 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); + } + + 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) => ( + + + {item.number}. {item.title} + + + {item.number} + + + + ))} + + + From 6c2d2e549c4859a4b8c02c553a7f9126ef279607 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Sat, 20 Dec 2025 10:21:28 +0100 Subject: [PATCH 05/44] added labels --- libs/ui/src/RadarChart.astro | 84 +++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/libs/ui/src/RadarChart.astro b/libs/ui/src/RadarChart.astro index 46f2b11..a759ced 100644 --- a/libs/ui/src/RadarChart.astro +++ b/libs/ui/src/RadarChart.astro @@ -43,10 +43,24 @@ const itemsByQuadrant = { // Calculate total size const totalWidth = quadrantSize * 2 + margin; const totalHeight = quadrantSize * 2 + margin; + +// Ring labels +const ringLabels = ["Adopt", "Trial", "Assess", "Hold"]; +const ringRadii = [0.25, 0.5, 0.75, 1.0]; // Percentages of quadrant size + +// Quadrant labels and their positions +const quadrantLabels = [ + { name: "Tools", position: "top-left" }, + { name: "Techniques", position: "top-right" }, + { name: "Platforms", position: "bottom-left" }, + { name: "Languages &
Frameworks", position: "bottom-right" }, +]; ---
-
+
+ +
+ + +
+ {ringLabels.map((label, index) => { + const radius = quadrantSize * ringRadii[index]; + const prevRadius = index > 0 ? quadrantSize * ringRadii[index - 1] : 0; + const ringCenter = prevRadius + (radius - prevRadius) / 2; + + return ( +
+ {label} +
+ ); + })} +
+ + +
+ {ringLabels.map((label, index) => { + const radius = quadrantSize * ringRadii[index]; + const prevRadius = index > 0 ? quadrantSize * ringRadii[index - 1] : 0; + const ringCenter = prevRadius + (radius - prevRadius) / 2; + + return ( +
+ {label} +
+ ); + })} +
+ + + {quadrantLabels.map((quadrant) => { + let positionStyle = ""; + let textAlign = ""; + + if (quadrant.position === "top-left") { + positionStyle = `top: 0; left: 0;`; + textAlign = "text-left"; + } else if (quadrant.position === "top-right") { + positionStyle = `top: 0; right: 0;`; + textAlign = "text-right"; + } else if (quadrant.position === "bottom-left") { + positionStyle = `bottom: 0; left: 0;`; + textAlign = "text-left"; + } else { + positionStyle = `bottom: 0; right: 0;`; + textAlign = "text-right"; + } + + // Get the color - need to handle the modified name with
+ const colorKey = quadrant.name.replace("
", " "); + + return ( +
+ ); + })} +
+ + From 6ba2cb701142fa57ef798e706fbe3af761a5da0d Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Sat, 20 Dec 2025 10:47:20 +0100 Subject: [PATCH 07/44] update radar item page --- .../pages/technology-radar/[radarItem].astro | 120 ++++++++++++------ 1 file changed, 80 insertions(+), 40 deletions(-) diff --git a/apps/dotnet/src/pages/technology-radar/[radarItem].astro b/apps/dotnet/src/pages/technology-radar/[radarItem].astro index e693b80..99b757f 100644 --- a/apps/dotnet/src/pages/technology-radar/[radarItem].astro +++ b/apps/dotnet/src/pages/technology-radar/[radarItem].astro @@ -26,6 +26,16 @@ export async function getStaticPaths() { } const radarItem: RadarItem = Astro.props; + +// Define colors for each quadrant (matching RadarChart) +const quadrantColors = { + "Techniques": "#3b82f6", // blue + "Tools": "#10b981", // green + "Platforms": "#f59e0b", // amber + "Languages & Frameworks": "#ef4444" // red +}; + +const quadrantColor = quadrantColors[radarItem.quadrant as keyof typeof quadrantColors] || "#3b82f6"; --- @@ -35,59 +45,89 @@ const radarItem: RadarItem = Astro.props;

{radarItem.title}

-

- {radarItem.description} -

+ +
+
+ + {radarItem.quadrant} + +
+
+ + {radarItem.ring} + +
+
{radarItem.tags && radarItem.tags.length > 0 && ( -
+
{radarItem.tags.map((tag) => ( - - #{tag.title} - +
+ + #{tag.title} + +
))}
)} - {radarItem.pros && radarItem.pros.length > 0 && ( -
-

Voordelen

-
    - {radarItem.pros.map((pro) => ( -
  • -

    + {radarItem.description} +

    + + {(radarItem.pros && radarItem.pros.length > 0) || (radarItem.cons && radarItem.cons.length > 0) ? ( +
    + {radarItem.pros && radarItem.pros.length > 0 && ( +
    +
    + -
    - {pro.title}. {pro.description} -
    -
  • - ))} -
-
- )} +

Pros

+
+
    + {radarItem.pros.map((pro) => ( +
  • + +
    + {pro.title.charAt(0).toUpperCase() + pro.title.slice(1)}: {pro.description.charAt(0).toUpperCase() + pro.description.slice(1)} +
    +
  • + ))} +
+
+ )} - {radarItem.cons && radarItem.cons.length > 0 && ( -
-

Nadelen

-
    - {radarItem.cons.map((con) => ( -
  • -
    +
    + -
    - {con.title}. {con.description} -
    -
  • - ))} -
+

Cons

+
+
    + {radarItem.cons.map((con) => ( +
  • + +
    + {con.title.charAt(0).toUpperCase() + con.title.slice(1)}: {con.description.charAt(0).toUpperCase() + con.description.slice(1)} +
    +
  • + ))} +
+
+ )} - )} + ) : null} {radarItem.conclusion && (
-

Conclusie

+

Conclusion

{radarItem.conclusion}

@@ -96,7 +136,7 @@ const radarItem: RadarItem = Astro.props;
From 5a74ea4c0c9a98836e2efbbca57bd5a7ca15ff05 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Sat, 20 Dec 2025 11:00:44 +0100 Subject: [PATCH 08/44] added rest of technology radar content --- apps/dotnet/src/pages/technology-radar/page/[page].astro | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/dotnet/src/pages/technology-radar/page/[page].astro b/apps/dotnet/src/pages/technology-radar/page/[page].astro index f177aa5..68e15f5 100644 --- a/apps/dotnet/src/pages/technology-radar/page/[page].astro +++ b/apps/dotnet/src/pages/technology-radar/page/[page].astro @@ -2,7 +2,7 @@ import type { GetStaticPaths } from "astro"; import { fetchData, type RadarItem, type Page as PageType } from "@xprtz/cms"; import Layout from "../../../layouts/layout.astro"; -import { Container, RadarChart } from "@xprtz/ui"; +import { Container, RadarChart, ComponentRenderer } from "@xprtz/ui"; export const getStaticPaths: GetStaticPaths = async () => { // Generate a single page for the radar @@ -16,6 +16,7 @@ const radarPages = await fetchData>({ query: { "filters[site][$eq]": site, "filters[slug][$eq]": "technology-radar", + "populate[components]": "*", status: "published", }, }); @@ -93,6 +94,9 @@ const allRadarItems = await fetchData>({
+
+ +
From 776f7b136a3c65882fd69116cf9c0b2ec9e7eab3 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Mon, 22 Dec 2025 13:44:17 +0100 Subject: [PATCH 09/44] added filtering by tag --- .../pages/technology-radar/page/[page].astro | 133 +++++++++++++++++- libs/ui/src/RadarQuadrant.astro | 2 +- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/apps/dotnet/src/pages/technology-radar/page/[page].astro b/apps/dotnet/src/pages/technology-radar/page/[page].astro index 68e15f5..fd5aca0 100644 --- a/apps/dotnet/src/pages/technology-radar/page/[page].astro +++ b/apps/dotnet/src/pages/technology-radar/page/[page].astro @@ -32,6 +32,15 @@ const allRadarItems = await fetchData>({ 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(); --- >({ {radarPage.title_website}

{radarPage.description}

-
+
+ {allTags.length > 0 && ( +
+ {allTags.map((tag) => ( + + ))} +
+ )}
@@ -102,3 +124,112 @@ const allRadarItems = await fetchData>({
+ + diff --git a/libs/ui/src/RadarQuadrant.astro b/libs/ui/src/RadarQuadrant.astro index 845aa05..9aff8c4 100644 --- a/libs/ui/src/RadarQuadrant.astro +++ b/libs/ui/src/RadarQuadrant.astro @@ -157,7 +157,7 @@ const positionedItems = items.map((item, index) => { {positionedItems.map((item) => ( - + {item.number}. {item.title} Date: Mon, 22 Dec 2025 14:26:01 +0100 Subject: [PATCH 10/44] fixed some styling issues --- .../pages/technology-radar/page/[page].astro | 18 ----------------- libs/ui/src/RadarQuadrant.astro | 20 +++++++++++++++++-- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/dotnet/src/pages/technology-radar/page/[page].astro b/apps/dotnet/src/pages/technology-radar/page/[page].astro index fd5aca0..2925866 100644 --- a/apps/dotnet/src/pages/technology-radar/page/[page].astro +++ b/apps/dotnet/src/pages/technology-radar/page/[page].astro @@ -130,22 +130,16 @@ const allTags = Array.from( let selectedTag = null; const initTagFiltering = () => { - console.log('Initializing tag filtering...'); const tagButtons = document.querySelectorAll('.tag-filter'); const radarContainer = document.getElementById('radar-container'); - console.log('Found tag buttons:', tagButtons.length); - console.log('Radar container:', radarContainer); - if (!radarContainer) { - console.warn('Radar container not found!'); return; } tagButtons.forEach(button => { button.addEventListener('click', () => { const tag = button.getAttribute('data-tag'); - console.log('Tag clicked:', tag); // Toggle tag selection if (selectedTag === tag) { @@ -177,46 +171,34 @@ const allTags = Array.from( }; const filterRadarItems = (tag) => { - console.log('Filtering radar items by tag:', tag); const radarItems = document.querySelectorAll('.radar-item'); - console.log('Found radar items:', radarItems.length); - console.log('All radar items data:', allRadarItems); radarItems.forEach((item, index) => { const link = item.querySelector('a'); if (!link) { - console.warn('No link found in radar item', index); return; } const href = link.getAttribute('href'); const slug = href?.replace('/technology-radar/', ''); - console.log(`Item ${index}: href=${href}, slug=${slug}`); // Find the corresponding radar item data const itemData = allRadarItems.find(ri => ri.slug === slug); if (!itemData) { - console.warn('No data found for slug:', slug); return; } - console.log(`Item ${index} data:`, itemData.title, 'tags:', itemData.tags); - if (!tag) { // Show all items item.setAttribute('style', 'transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;'); - console.log(`Showing item ${index}`); } else { // Check if item has the selected tag const hasTag = itemData.tags?.some(t => t.title === tag); - console.log(`Item ${index} has tag ${tag}:`, hasTag); if (hasTag) { item.setAttribute('style', 'transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;'); - console.log(`Showing item ${index}`); } else { item.setAttribute('style', 'transition: opacity 0.3s ease; opacity: 0.2; pointer-events: none;'); - console.log(`Hiding item ${index}`); } } }); diff --git a/libs/ui/src/RadarQuadrant.astro b/libs/ui/src/RadarQuadrant.astro index 9aff8c4..edc5d21 100644 --- a/libs/ui/src/RadarQuadrant.astro +++ b/libs/ui/src/RadarQuadrant.astro @@ -126,6 +126,10 @@ const positionedItems = items.map((item, index) => { y = origin.y + radius * Math.sin(angle); } + // Clamp positions to keep circles within SVG bounds (considering circle radius) + x = Math.max(circleRadius, Math.min(size - circleRadius, x)); + y = Math.max(circleRadius, Math.min(size - circleRadius, y)); + return { ...item, x, y }; }); --- @@ -135,6 +139,7 @@ const positionedItems = items.map((item, index) => { height={size} viewBox={`0 0 ${size} ${size}`} class="radar-quadrant" + style={`--quadrant-color: ${color};`} > {rings.slice().reverse().map((ringData, index) => { @@ -167,15 +172,16 @@ const positionedItems = items.map((item, index) => { fill="white" stroke={color} stroke-width="2" - class="cursor-pointer hover:fill-gray-100 transition-colors" + class="cursor-pointer radar-item-circle transition-all" /> {item.number} @@ -188,4 +194,14 @@ const positionedItems = items.map((item, index) => { .radar-quadrant { display: block; } + + .radar-item-circle { + transition: all 0.2s ease; + } + + .radar-item:hover .radar-item-circle { + fill: color-mix(in srgb, var(--quadrant-color) 10%, white); + stroke-width: 3; + r: 14; + } From f10ab194929bdeb62a9bd8b7691ee951832073c0 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Mon, 22 Dec 2025 15:34:29 +0100 Subject: [PATCH 11/44] zoom in radar quadrant --- libs/ui/src/RadarChart.astro | 155 +++++++++++++++++++++++++++++++++-- 1 file changed, 147 insertions(+), 8 deletions(-) diff --git a/libs/ui/src/RadarChart.astro b/libs/ui/src/RadarChart.astro index c2f30b1..522d516 100644 --- a/libs/ui/src/RadarChart.astro +++ b/libs/ui/src/RadarChart.astro @@ -103,7 +103,7 @@ const quadrantLabels = [ -
+
{ringLabels.map((label, index) => { const radius = quadrantSize * ringRadii[index]; const prevRadius = index > 0 ? quadrantSize * ringRadii[index - 1] : 0; @@ -121,7 +121,7 @@ const quadrantLabels = [
-
+
{ringLabels.map((label, index) => { const radius = quadrantSize * ringRadii[index]; const prevRadius = index > 0 ? quadrantSize * ringRadii[index - 1] : 0; @@ -200,6 +200,87 @@ const quadrantLabels = [ .radar-chart.hovering .quadrant-label.active { opacity: 1; } + + /* Zoom functionality */ + .quadrant-wrapper { + cursor: pointer; + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: center; + position: relative; + } + + .radar-chart.zoomed .quadrant-wrapper { + opacity: 0; + pointer-events: none; + } + + .radar-chart.zoomed .quadrant-wrapper.zoomed-in { + opacity: 1; + pointer-events: auto; + transform: scale(2) translate(0, 0); + z-index: 10; + } + + /* Position adjustments for each quadrant when zoomed */ + .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="tools"] { + transform: scale(2) translate(25%, 25%); + } + + .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="techniques"] { + transform: scale(2) translate(-25%, 25%); + } + + .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="platforms"] { + transform: scale(2) translate(25%, -25%); + } + + .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="frameworks"] { + transform: scale(2) translate(-25%, -25%); + } + + .radar-chart.zoomed .quadrant-label { + opacity: 0; + } + + .radar-chart.zoomed .quadrant-label.zoomed-in { + opacity: 1; + } + + /* Ring labels positioning and transitions */ + .ring-labels { + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: center; + } + + /* Hide ring labels by default when zoomed */ + .radar-chart.zoomed .ring-labels { + opacity: 0; + pointer-events: none; + } + + /* Show and transform ring labels for Tools quadrant (top-left) */ + .radar-chart.zoomed.zoom-tools .ring-labels[data-ring-labels="left"] { + opacity: 1; + transform: scale(2) translate(25%, 205px); + } + + /* Show and transform ring labels for Platforms quadrant (bottom-left) */ + .radar-chart.zoomed.zoom-platforms .ring-labels[data-ring-labels="left"] { + opacity: 1; + transform: scale(2) translate(25%, -205px); + } + + /* Show and transform ring labels for Techniques quadrant (top-right) */ + .radar-chart.zoomed.zoom-techniques .ring-labels[data-ring-labels="right"] { + opacity: 1; + transform: scale(2) translate(-25%, 205px); + } + + /* Show and transform ring labels for Frameworks quadrant (bottom-right) */ + .radar-chart.zoomed.zoom-frameworks .ring-labels[data-ring-labels="right"] { + opacity: 1; + transform: scale(2) translate(-25%, -205px); + } From 28c8c4ed4de19dc44855410f9c56bad856a96901 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Mon, 22 Dec 2025 16:38:21 +0100 Subject: [PATCH 12/44] added quadrant selection --- .../pages/technology-radar/page/[page].astro | 106 ++--- libs/ui/index.ts | 2 + libs/ui/src/RadarChart.astro | 383 ++++++++++++------ libs/ui/src/RadarQuadrantItemList.astro | 132 ++++++ 4 files changed, 442 insertions(+), 181 deletions(-) create mode 100644 libs/ui/src/RadarQuadrantItemList.astro diff --git a/apps/dotnet/src/pages/technology-radar/page/[page].astro b/apps/dotnet/src/pages/technology-radar/page/[page].astro index 2925866..1e436a6 100644 --- a/apps/dotnet/src/pages/technology-radar/page/[page].astro +++ b/apps/dotnet/src/pages/technology-radar/page/[page].astro @@ -36,17 +36,12 @@ const allRadarItems = await fetchData>({ // Extract all unique tags from radar items const allTags = Array.from( new Set( - allRadarItems - .flatMap(item => item.tags || []) - .map(tag => tag.title) + allRadarItems.flatMap((item) => item.tags || []).map((tag) => tag.title) ) ).sort(); --- - +
- {allTags.length > 0 && ( -
- {allTags.map((tag) => ( - - ))} -
- )} + { + allTags.length > 0 && ( +
+ {allTags.map((tag) => ( + + ))} +
+ ) + }
@@ -126,41 +126,41 @@ const allTags = Array.from( diff --git a/libs/ui/index.ts b/libs/ui/index.ts index e294f2e..5b07e57 100644 --- a/libs/ui/index.ts +++ b/libs/ui/index.ts @@ -21,6 +21,7 @@ import Quote from "./src/Quote.astro"; import Blogs from "./src/Blogs.astro"; import RadarQuadrant from "./src/RadarQuadrant.astro"; import RadarChart from "./src/RadarChart.astro"; +import RadarQuadrantItemList from "./src/RadarQuadrantItemList.astro"; import ComponentRenderer from "./src/ComponentRenderer.astro"; @@ -48,5 +49,6 @@ export { Blogs, RadarQuadrant, RadarChart, + RadarQuadrantItemList, ComponentRenderer }; diff --git a/libs/ui/src/RadarChart.astro b/libs/ui/src/RadarChart.astro index 522d516..48dcab2 100644 --- a/libs/ui/src/RadarChart.astro +++ b/libs/ui/src/RadarChart.astro @@ -1,5 +1,6 @@ --- import RadarQuadrant from "./RadarQuadrant.astro"; +import RadarQuadrantItemList from "./RadarQuadrantItemList.astro"; import type { RadarItem } from "@xprtz/cms"; interface Props { @@ -21,23 +22,25 @@ const margin = 20; // Define colors for each quadrant const quadrantColors = { - "Techniques": "#3b82f6", // blue - "Tools": "#10b981", // green - "Platforms": "#f59e0b", // amber - "Languages & Frameworks": "#ef4444" // red + Techniques: "#3b82f6", // blue + Tools: "#10b981", // green + Platforms: "#f59e0b", // amber + "Languages & Frameworks": "#ef4444", // red }; // Add numbers to items and group by quadrant const itemsWithNumbers = items.map((item, index) => ({ ...item, - number: index + 1 + number: index + 1, })); const itemsByQuadrant = { - "Techniques": itemsWithNumbers.filter(item => item.quadrant === "Techniques"), - "Tools": itemsWithNumbers.filter(item => item.quadrant === "Tools"), - "Platforms": itemsWithNumbers.filter(item => item.quadrant === "Platforms"), - "Languages & Frameworks": itemsWithNumbers.filter(item => item.quadrant === "Languages & Frameworks") + Techniques: itemsWithNumbers.filter((item) => item.quadrant === "Techniques"), + Tools: itemsWithNumbers.filter((item) => item.quadrant === "Tools"), + Platforms: itemsWithNumbers.filter((item) => item.quadrant === "Platforms"), + "Languages & Frameworks": itemsWithNumbers.filter( + (item) => item.quadrant === "Languages & Frameworks" + ), }; // Calculate total size @@ -57,129 +60,194 @@ const quadrantLabels = [ ]; --- -
-
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
- {ringLabels.map((label, index) => { - const radius = quadrantSize * ringRadii[index]; - const prevRadius = index > 0 ? quadrantSize * ringRadii[index - 1] : 0; - const ringCenter = prevRadius + (radius - prevRadius) / 2; +
+
+
+ +
+ +
+ +
- return ( +
- {label} +
- ); - })} -
- -
- {ringLabels.map((label, index) => { - const radius = quadrantSize * ringRadii[index]; - const prevRadius = index > 0 ? quadrantSize * ringRadii[index - 1] : 0; - const ringCenter = prevRadius + (radius - prevRadius) / 2; + +
+ +
- return ( +
- {label} +
- ); - })} -
+
- - {quadrantLabels.map((quadrant, index) => { - let positionStyle = ""; - let textAlign = ""; - let quadrantKey = ""; - - if (quadrant.position === "top-left") { - positionStyle = `top: 0; left: 0;`; - textAlign = "text-left"; - quadrantKey = "tools"; - } else if (quadrant.position === "top-right") { - positionStyle = `top: 0; right: 0;`; - textAlign = "text-right"; - quadrantKey = "techniques"; - } else if (quadrant.position === "bottom-left") { - positionStyle = `bottom: 0; left: 0;`; - textAlign = "text-left"; - quadrantKey = "platforms"; - } else { - positionStyle = `bottom: 0; right: 0;`; - textAlign = "text-right"; - quadrantKey = "frameworks"; - } - - // Get the color - need to handle the modified name with
- const colorKey = quadrant.name.replace("
", " "); - - return ( + +
+ { + ringLabels.map((label, index) => { + const radius = quadrantSize * ringRadii[index]; + const prevRadius = + index > 0 ? quadrantSize * ringRadii[index - 1] : 0; + const ringCenter = prevRadius + (radius - prevRadius) / 2; + + return ( +
+ {label} +
+ ); + }) + } +
+ +
- ); - })} + class="absolute right-0 flex justify-center items-center ring-labels transition-opacity duration-300" + data-ring-labels="right" + style={`top: ${quadrantSize}px; height: ${margin}px; width: ${quadrantSize}px;`} + > + { + ringLabels.map((label, index) => { + const radius = quadrantSize * ringRadii[index]; + const prevRadius = + index > 0 ? quadrantSize * ringRadii[index - 1] : 0; + const ringCenter = prevRadius + (radius - prevRadius) / 2; + + return ( +
+ {label} +
+ ); + }) + } +
+ + + { + quadrantLabels.map((quadrant, index) => { + let positionStyle = ""; + let textAlign = ""; + let quadrantKey = ""; + + if (quadrant.position === "top-left") { + positionStyle = `top: 0; left: 0;`; + textAlign = "text-left"; + quadrantKey = "tools"; + } else if (quadrant.position === "top-right") { + positionStyle = `top: 0; right: 0;`; + textAlign = "text-right"; + quadrantKey = "techniques"; + } else if (quadrant.position === "bottom-left") { + positionStyle = `bottom: 0; left: 0;`; + textAlign = "text-left"; + quadrantKey = "platforms"; + } else { + positionStyle = `bottom: 0; right: 0;`; + textAlign = "text-right"; + quadrantKey = "frameworks"; + } + + // Get the color - need to handle the modified name with
+ const colorKey = quadrant.name.replace("
", " "); + + return ( +
+ ); + }) + } +
+ + + + + +
@@ -328,6 +392,10 @@ const quadrantLabels = [ radarCharts.forEach((chart) => { const quadrants = chart.querySelectorAll(".quadrant-wrapper"); const labels = chart.querySelectorAll(".quadrant-label"); + const wrapper = chart.closest(".radar-chart-wrapper"); + + // Get all quadrant item lists in this wrapper + const itemLists = wrapper?.querySelectorAll(".quadrant-item-list"); quadrants.forEach((quadrant) => { const quadrantKey = quadrant.getAttribute("data-quadrant"); @@ -350,15 +418,26 @@ const quadrantLabels = [ const isCurrentlyZoomed = quadrant.classList.contains("zoomed-in"); if (isCurrentlyZoomed) { - // Zoom out - remove all zoom classes + // Zoom out - remove all zoom classes and hide list chart.classList.remove("zoomed"); - chart.classList.remove("zoom-tools", "zoom-techniques", "zoom-platforms", "zoom-frameworks"); + chart.classList.remove( + "zoom-tools", + "zoom-techniques", + "zoom-platforms", + "zoom-frameworks" + ); quadrants.forEach((q) => q.classList.remove("zoomed-in")); labels.forEach((l) => l.classList.remove("zoomed-in")); + itemLists?.forEach((list) => list.classList.add("hidden")); } else { // Zoom in - add zoom class to chart and this quadrant chart.classList.add("zoomed"); - chart.classList.remove("zoom-tools", "zoom-techniques", "zoom-platforms", "zoom-frameworks"); + chart.classList.remove( + "zoom-tools", + "zoom-techniques", + "zoom-platforms", + "zoom-frameworks" + ); chart.classList.add(`zoom-${quadrantKey}`); quadrants.forEach((q) => q.classList.remove("zoomed-in")); labels.forEach((l) => l.classList.remove("zoomed-in")); @@ -366,9 +445,42 @@ const quadrantLabels = [ if (correspondingLabel) { correspondingLabel.classList.add("zoomed-in"); } + + // Show the corresponding item list + itemLists?.forEach((list) => { + const listQuadrant = list + .querySelector("h3") + ?.textContent?.trim(); + const quadrantName = + quadrantKey === "frameworks" + ? "Languages & Frameworks" + : quadrantKey.charAt(0).toUpperCase() + quadrantKey.slice(1); + if (listQuadrant === quadrantName) { + list.classList.remove("hidden"); + } else { + list.classList.add("hidden"); + } + }); } }); }); + + // Handle back to radar button clicks + const backButtons = wrapper?.querySelectorAll(".back-to-radar"); + backButtons?.forEach((button) => { + button.addEventListener("click", () => { + chart.classList.remove("zoomed"); + chart.classList.remove( + "zoom-tools", + "zoom-techniques", + "zoom-platforms", + "zoom-frameworks" + ); + quadrants.forEach((q) => q.classList.remove("zoomed-in")); + labels.forEach((l) => l.classList.remove("zoomed-in")); + itemLists?.forEach((list) => list.classList.add("hidden")); + }); + }); }); }; @@ -381,7 +493,10 @@ const quadrantLabels = [ document.addEventListener("astro:page-load", initRadar); // Also initialize immediately in case the event already fired - if (document.readyState === "complete" || document.readyState === "interactive") { + if ( + document.readyState === "complete" || + document.readyState === "interactive" + ) { initRadar(); } diff --git a/libs/ui/src/RadarQuadrantItemList.astro b/libs/ui/src/RadarQuadrantItemList.astro new file mode 100644 index 0000000..37804c1 --- /dev/null +++ b/libs/ui/src/RadarQuadrantItemList.astro @@ -0,0 +1,132 @@ +--- +interface Props { + /** + * Array of radar items for the selected quadrant + */ + items: object[]; + + /** + * Name of the quadrant + */ + quadrantName: string; + + /** + * Color for the quadrant + */ + color: string; +} + +const { items, quadrantName, color } = Astro.props; + +// Group items by ring +const ringOrder = ["Adopt", "Trial", "Assess", "Hold"]; +const itemsByRing = ringOrder.reduce( + (acc, ring) => { + const ringItems = items.filter((item) => item.ring === ring); + if (ringItems.length > 0) { + acc[ring] = ringItems; + } + return acc; + }, + {} as Record +); +--- + +
+ + From 893d94a921c74534ab5be0825eb6380227ba7989 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Tue, 23 Dec 2025 10:22:36 +0100 Subject: [PATCH 13/44] added radar item with number type --- libs/cms/AGENTS.md | 32 ++++++++++++++++ libs/cms/index.ts | 3 +- libs/cms/models/radarItem.ts | 4 ++ libs/ui/AGENTS.md | 50 +++++++++++++++++++++---- libs/ui/src/RadarChart.astro | 4 +- libs/ui/src/RadarQuadrantItemList.astro | 6 ++- 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/libs/cms/AGENTS.md b/libs/cms/AGENTS.md index 01aef29..481466a 100644 --- a/libs/cms/AGENTS.md +++ b/libs/cms/AGENTS.md @@ -273,6 +273,38 @@ export type AlgemeneVoorwaarden = { } ``` +### Technology Radar Models + +#### [radarItem.ts](models/radarItem.ts) +Technology radar items with adoption stages: +```typescript +export type RadarQuadrant = "Techniques" | "Tools" | "Platforms" | "Languages & Frameworks"; +export type RadarRing = "Adopt" | "Trial" | "Assess" | "Hold"; + +export type RadarItem = { + slug: string; + quadrant: RadarQuadrant; + ring: RadarRing; + title: string; + description: string; + pros: ListItem[]; + cons: ListItem[]; + conclusion: string; + tags: Tag[]; +} + +export type RadarItemWithNumber = RadarItem & { + number: number; +} +``` + +**Important Notes:** +- `RadarItem` represents the base type from CMS without numbers +- `RadarItemWithNumber` extends `RadarItem` with a `number` property added during rendering +- Numbers are assigned sequentially by the UI layer, not stored in CMS +- Use `RadarItem` when fetching from CMS +- Use `RadarItemWithNumber` when passing items to UI components that need numbering + ## API Wrapper ### fetchData Function diff --git a/libs/cms/index.ts b/libs/cms/index.ts index 1f8b7c9..4d2e1ff 100644 --- a/libs/cms/index.ts +++ b/libs/cms/index.ts @@ -17,7 +17,7 @@ import { Team } from "./models/team.js"; import { Directors } from "./models/directors.js"; import { Social } from "./models/social.js"; import { AlgemeneVoorwaarden } from "./models/algemeneVoorwaarden.js"; -import { RadarItem, RadarQuadrant, RadarRing } from "./models/radarItem.js"; +import { RadarItem, RadarItemWithNumber, RadarQuadrant, RadarRing } from "./models/radarItem.js"; import { HomePage } from "./models/homePage.js"; @@ -46,6 +46,7 @@ export { type Directors, type HomePage, type RadarItem, + type RadarItemWithNumber, type RadarQuadrant, type RadarRing }; diff --git a/libs/cms/models/radarItem.ts b/libs/cms/models/radarItem.ts index d047aa9..f3ff700 100644 --- a/libs/cms/models/radarItem.ts +++ b/libs/cms/models/radarItem.ts @@ -15,3 +15,7 @@ export type RadarItem = { conclusion: string; tags: Tag[]; } + +export type RadarItemWithNumber = RadarItem & { + number: number; +} diff --git a/libs/ui/AGENTS.md b/libs/ui/AGENTS.md index ce3a01b..1b17783 100644 --- a/libs/ui/AGENTS.md +++ b/libs/ui/AGENTS.md @@ -57,6 +57,7 @@ libs/ui/ ### Technology Radar Components - [RadarChart.astro](src/RadarChart.astro) - Technology radar visualization with four quadrants - [RadarQuadrant.astro](src/RadarQuadrant.astro) - Individual radar quadrant with concentric rings and items +- [RadarQuadrantItemList.astro](src/RadarQuadrantItemList.astro) - List view of items in a selected quadrant ### Utility Components - [LogoCloud.astro](src/LogoCloud.astro) - Client/partner logo display @@ -400,7 +401,7 @@ Individual quadrant representing 90° of the radar (one quarter circle). - `position: 0 | 1 | 2 | 3` - Quadrant position (0: 0-90°, 1: 90-180°, 2: 180-270°, 3: 270-360°) - `color: string` - Color for the quadrant - `size?: number` - Size in pixels (default: 400) -- `items?: ItemWithNumber[]` - Radar items to display +- `items?: RadarItemWithNumber[]` - Radar items to display **Features:** - Four concentric rings representing adoption stages: @@ -435,19 +436,52 @@ import { RadarQuadrant } from "@xprtz/ui"; /> ``` -### RadarItem Type +### RadarItem Types + +The radar system uses two related types: -Items require the following structure: ```typescript -interface ItemWithNumber extends RadarItem { +// 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...) - title: string; // Display title - slug: string; // URL slug for detail page - ring: RadarRing; // "Adopt" | "Trial" | "Assess" | "Hold" - quadrant: RadarQuadrant; // Quadrant name } ``` +**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 + +**Features:** +- Groups items by ring (Adopt, Trial, Assess, Hold) +- Displays item number, title, and description +- Links to item detail pages +- Scrollable list with sticky ring headers +- "Back to radar" button to return to main view +- Hidden by default, shown when quadrant is clicked + ### Integration Example ```astro diff --git a/libs/ui/src/RadarChart.astro b/libs/ui/src/RadarChart.astro index 48dcab2..9d6cf46 100644 --- a/libs/ui/src/RadarChart.astro +++ b/libs/ui/src/RadarChart.astro @@ -1,7 +1,7 @@ --- import RadarQuadrant from "./RadarQuadrant.astro"; import RadarQuadrantItemList from "./RadarQuadrantItemList.astro"; -import type { RadarItem } from "@xprtz/cms"; +import type { RadarItem, RadarItemWithNumber } from "@xprtz/cms"; interface Props { /** @@ -29,7 +29,7 @@ const quadrantColors = { }; // Add numbers to items and group by quadrant -const itemsWithNumbers = items.map((item, index) => ({ +const itemsWithNumbers: RadarItemWithNumber[] = items.map((item, index) => ({ ...item, number: index + 1, })); diff --git a/libs/ui/src/RadarQuadrantItemList.astro b/libs/ui/src/RadarQuadrantItemList.astro index 37804c1..9244360 100644 --- a/libs/ui/src/RadarQuadrantItemList.astro +++ b/libs/ui/src/RadarQuadrantItemList.astro @@ -1,9 +1,11 @@ --- +import type { RadarItemWithNumber } from "@xprtz/cms"; + interface Props { /** * Array of radar items for the selected quadrant */ - items: object[]; + items: RadarItemWithNumber[]; /** * Name of the quadrant @@ -28,7 +30,7 @@ const itemsByRing = ringOrder.reduce( } return acc; }, - {} as Record + {} as Record ); --- From 1798e8f1d641e428d8d2586ddb171ac584cf27de Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Tue, 23 Dec 2025 10:35:05 +0100 Subject: [PATCH 14/44] improved animation --- libs/ui/AGENTS.md | 7 ++ libs/ui/src/RadarChart.astro | 113 ++++++++++++++++-------- libs/ui/src/RadarQuadrantItemList.astro | 11 ++- 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/libs/ui/AGENTS.md b/libs/ui/AGENTS.md index 1b17783..39d4a13 100644 --- a/libs/ui/AGENTS.md +++ b/libs/ui/AGENTS.md @@ -385,6 +385,10 @@ The main radar visualization that combines four quadrants into a complete circle - **Platforms**: Amber (#f59e0b) - **Languages & Frameworks**: Red (#ef4444) - 20px margin between quadrants for visual separation +- Interactive zoom functionality with smooth animations (500ms duration) +- Transform-based container slide (200px left) prevents layout reflow +- List slides in from right after zoom completes +- Synchronized animations create smooth push-effect without jumping **Usage:** ```astro @@ -481,6 +485,9 @@ List view displaying all items within a selected quadrant, grouped by ring. - Scrollable list with sticky ring headers - "Back to radar" button to return to main view - Hidden by default, shown when quadrant is clicked +- Smooth slide-in animation from right (400ms duration) +- Appears after quadrant zoom animation completes (500ms delay) +- Coordinated slide-out animation when zooming out ### Integration Example diff --git a/libs/ui/src/RadarChart.astro b/libs/ui/src/RadarChart.astro index 9d6cf46..e440656 100644 --- a/libs/ui/src/RadarChart.astro +++ b/libs/ui/src/RadarChart.astro @@ -241,9 +241,9 @@ const quadrantLabels = [ - - diff --git a/libs/ui/src/TechnologyRadar.astro b/libs/ui/src/TechnologyRadar.astro deleted file mode 100644 index aed3e46..0000000 --- a/libs/ui/src/TechnologyRadar.astro +++ /dev/null @@ -1,163 +0,0 @@ ---- -import { fetchData, type RadarItem } from "@xprtz/cms"; -import { RadarChart } from "@xprtz/ui"; - -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/RadarChart.astro b/libs/ui/src/radar/RadarChart.astro new file mode 100644 index 0000000..867342e --- /dev/null +++ b/libs/ui/src/radar/RadarChart.astro @@ -0,0 +1,933 @@ +--- +import RadarQuadrant from "./RadarQuadrant.astro"; +import RadarQuadrantItemList from "./RadarQuadrantItemList.astro"; +import type { RadarItem, RadarItemWithNumber } from "@xprtz/cms"; +import { RINGS } 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; + +// Quadrant configuration +const QUADRANTS = { + Tools: { + color: "#10b981", // green + key: "tools", + gridPosition: "top-left" as const, + }, + Technieken: { + color: "#3b82f6", // blue + key: "techniques", + gridPosition: "top-right" as const, + }, + Platformen: { + color: "#f59e0b", // amber + key: "platforms", + gridPosition: "bottom-left" as const, + }, + "Talen & Frameworks": { + color: "#ef4444", // red + key: "frameworks", + gridPosition: "bottom-right" as const, + }, +} as const; + +type QuadrantName = keyof typeof QUADRANTS; + +// 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; + +// Calculate total size +const totalSize = quadrantSize * 2 + margin; + +// 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" }, +}; +--- + +
+
+
+ +
+ { + Object.entries(QUADRANTS).map(([name, config]) => { + const flexClasses = flexClassMap[config.gridPosition]; + return ( +
+ +
+ ); + }) + } +
+ + +
+ { + 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; + const leftPosition = quadrantSize - ringCenter; + const labelStyle = `left: ${leftPosition}px; height: ${margin}px;`; + + return ( +
+ {ring.label} +
+ ); + }) + } +
+ + +
+ { + 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; + const labelStyle = `left: ${ringCenter}px; height: ${margin}px;`; + + return ( +
+ {ring.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/RadarQuadrant.astro b/libs/ui/src/radar/RadarQuadrant.astro similarity index 81% rename from libs/ui/src/RadarQuadrant.astro rename to libs/ui/src/radar/RadarQuadrant.astro index edb6da4..cb60fb8 100644 --- a/libs/ui/src/RadarQuadrant.astro +++ b/libs/ui/src/radar/RadarQuadrant.astro @@ -1,15 +1,18 @@ --- import type { RadarRing, RadarItem } from "@xprtz/cms"; +import { buildRadarItemLink, RINGS } from "./radarUtils"; interface ItemWithNumber extends RadarItem { number: number; } +type GridPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right"; + interface Props { /** - * Which quadrant position (0: 0-90°, 1: 90-180°, 2: 180-270°, 3: 270-360°) + * Grid position of the quadrant */ - position: 0 | 1 | 2 | 3; + gridPosition: GridPosition; /** * The color of the quadrant @@ -32,17 +35,21 @@ interface Props { parentPageSlug?: string; } -const { position, color, size = 400, items = [], parentPageSlug } = Astro.props; +const { gridPosition, color, size = 400, items = [], parentPageSlug } = Astro.props; -// Define the four rings from center to outside -const rings: { ring: RadarRing; radiusPercent: number }[] = [ - { ring: "Adopt", radiusPercent: 25 }, - { ring: "Trial", radiusPercent: 50 }, - { ring: "Assess", radiusPercent: 75 }, - { ring: "Hold", radiusPercent: 100 }, -]; +// 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° +}; -// Determine origin point based on quadrant position +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 @@ -85,12 +92,12 @@ const circleRadius = 12; // Function to get radius range for each ring const getRingRadiusRange = (ring: RadarRing): { min: number; max: number } => { - const ringData = rings.find((r) => r.ring === ring); + const ringData = RINGS.find((r) => r.label === ring); if (!ringData) return { min: 0, max: size * 0.25 }; - const index = rings.findIndex((r) => r.ring === ring); - const minPercent = index > 0 ? rings[index - 1].radiusPercent : 0; - const maxPercent = ringData.radiusPercent; + 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), @@ -148,18 +155,18 @@ const positionedItems = items.map((item, index) => { > { - rings + RINGS .slice() .reverse() .map((ringData, index) => { - const actualIndex = rings.length - 1 - 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) => { - // Build the link with optional parent page query parameter - const itemLink = parentPageSlug - ? `/radar-items/${item.slug}?from=${encodeURIComponent(parentPageSlug)}` - : `/radar-items/${item.slug}`; + 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} diff --git a/libs/ui/src/RadarQuadrantItemList.astro b/libs/ui/src/radar/RadarQuadrantItemList.astro similarity index 83% rename from libs/ui/src/RadarQuadrantItemList.astro rename to libs/ui/src/radar/RadarQuadrantItemList.astro index d48ebe9..a54e633 100644 --- a/libs/ui/src/RadarQuadrantItemList.astro +++ b/libs/ui/src/radar/RadarQuadrantItemList.astro @@ -1,5 +1,6 @@ --- import type { RadarItemWithNumber } from "@xprtz/cms"; +import { buildRadarItemLink, RINGS } from "./radarUtils"; interface Props { /** @@ -26,12 +27,11 @@ interface Props { const { items, quadrantName, color, parentPageSlug } = Astro.props; // Group items by ring -const ringOrder = ["Adopt", "Trial", "Assess", "Hold"]; -const itemsByRing = ringOrder.reduce( - (acc, ring) => { - const ringItems = items.filter((item) => item.ring === ring); +const itemsByRing = RINGS.reduce( + (acc, { label }) => { + const ringItems = items.filter((item) => item.ring === label); if (ringItems.length > 0) { - acc[ring] = ringItems; + acc[label] = ringItems; } return acc; }, @@ -78,10 +78,7 @@ const itemsByRing = ringOrder.reduce( </h4> <ul class="space-y-2"> {ringItems.map((item) => { - // Build the link with optional parent page query parameter - const itemLink = parentPageSlug - ? `/radar-items/${item.slug}?from=${encodeURIComponent(parentPageSlug)}` - : `/radar-items/${item.slug}`; + const itemLink = buildRadarItemLink(item.slug, parentPageSlug); return ( <li> @@ -115,8 +112,8 @@ const itemsByRing = ringOrder.reduce( transform: translateX(100%); opacity: 0; transition: - transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), - opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s; + transform var(--slide-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1), + opacity var(--fade-out-duration, 300ms) cubic-bezier(0.4, 0, 0.2, 1) var(--fade-out-delay, 100ms); } .quadrant-item-list.hidden { @@ -127,8 +124,8 @@ const itemsByRing = ringOrder.reduce( transform: translateX(0); opacity: 1; transition: - transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), - opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1); + transform var(--slide-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1), + opacity var(--fade-in-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1); } .line-clamp-2 { diff --git a/libs/ui/src/radar/TechnologyRadar.astro b/libs/ui/src/radar/TechnologyRadar.astro new file mode 100644 index 0000000..a5833da --- /dev/null +++ b/libs/ui/src/radar/TechnologyRadar.astro @@ -0,0 +1,141 @@ +--- +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<Array<RadarItem>>({ + 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(); +--- + +<h2 + class="mt-12 text-pretty text-3xl font-semibold tracking-tight text-primary-600" +> + {title} +</h2> +<div class="mt-10 flex justify-center" id="radar-container"> + <RadarChart items={allRadarItems} parentPageSlug={currentPageSlug} /> +</div> +{ + allTags.length > 0 && ( + <div + class="mt-10 flex flex-wrap justify-center gap-3" + id="tag-filter-container" + > + {allTags.map((tag) => ( + <button + type="button" + class="tag-filter inline-flex items-center rounded-xl bg-primary-100 px-4 py-1 text-sm/6 font-medium text-primary-700 hover:bg-primary-200 transition-colors cursor-pointer" + data-tag={tag} + > + #{tag} + </button> + ))} + </div> + ) +} + +<script> + import { initializeOnReady } from "./radarUtils"; + + let selectedTag: string | null = null; + + const initTagFiltering = () => { + const tagButtons = document.querySelectorAll(".tag-filter"); + const radarContainer = document.getElementById("radar-container"); + + if (!radarContainer) { + return; + } + + tagButtons.forEach((button) => { + button.addEventListener("click", () => { + const tag = button.getAttribute("data-tag"); + + // Toggle tag selection + if (selectedTag === tag) { + selectedTag = null; + // Reset all buttons + tagButtons.forEach((btn) => { + btn.classList.remove("bg-primary-600", "text-white"); + btn.classList.add("bg-primary-100", "text-primary-700"); + }); + // Show all items + filterRadarItems(null); + } else { + selectedTag = tag; + // Update button states + tagButtons.forEach((btn) => { + if (btn.getAttribute("data-tag") === tag) { + btn.classList.remove("bg-primary-100", "text-primary-700"); + btn.classList.add("bg-primary-600", "text-white"); + } else { + btn.classList.remove("bg-primary-600", "text-white"); + btn.classList.add("bg-primary-100", "text-primary-700"); + } + }); + // Filter items + filterRadarItems(tag); + } + }); + }); + }; + + const filterRadarItems = (tag: string | null) => { + const radarItems = document.querySelectorAll(".radar-item"); + + radarItems.forEach((item) => { + // Get tags from data attribute + const tagsAttr = item.getAttribute("data-tags"); + const tags: string[] = tagsAttr ? JSON.parse(tagsAttr) : []; + + if (!tag) { + // Show all items + item.setAttribute( + "style", + "transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;" + ); + } else { + // Check if item has the selected tag + const hasTag = tags.includes(tag); + if (hasTag) { + item.setAttribute( + "style", + "transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;" + ); + } else { + item.setAttribute( + "style", + "transition: opacity 0.3s ease; opacity: 0.2; pointer-events: none;" + ); + } + } + }); + }; + + // Initialize on page load + initializeOnReady(initTagFiltering); +</script> diff --git a/libs/ui/src/radar/radarUtils.ts b/libs/ui/src/radar/radarUtils.ts new file mode 100644 index 0000000..e187766 --- /dev/null +++ b/libs/ui/src/radar/radarUtils.ts @@ -0,0 +1,39 @@ +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; + +/** + * 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(); + } +} From 3a3d790123379aeb4bf602cb80ccf526d10d1179 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 16:03:08 +0100 Subject: [PATCH 32/44] refactor quadrants config --- libs/ui/src/radar/RadarChart.astro | 53 ++------------------------- libs/ui/src/radar/RadarQuadrant.astro | 18 ++++----- libs/ui/src/radar/radarUtils.ts | 33 +++++++++++++++++ 3 files changed, 43 insertions(+), 61 deletions(-) diff --git a/libs/ui/src/radar/RadarChart.astro b/libs/ui/src/radar/RadarChart.astro index 867342e..c5f4e39 100644 --- a/libs/ui/src/radar/RadarChart.astro +++ b/libs/ui/src/radar/RadarChart.astro @@ -2,7 +2,7 @@ import RadarQuadrant from "./RadarQuadrant.astro"; import RadarQuadrantItemList from "./RadarQuadrantItemList.astro"; import type { RadarItem, RadarItemWithNumber } from "@xprtz/cms"; -import { RINGS } from "./radarUtils"; +import { RINGS, QUADRANTS, type QuadrantName } from "./radarUtils"; interface Props { /** @@ -26,32 +26,6 @@ const { items = [], quadrantSize = 400, parentPageSlug } = Astro.props; // Margin between quadrants const margin = 20; -// Quadrant configuration -const QUADRANTS = { - Tools: { - color: "#10b981", // green - key: "tools", - gridPosition: "top-left" as const, - }, - Technieken: { - color: "#3b82f6", // blue - key: "techniques", - gridPosition: "top-right" as const, - }, - Platformen: { - color: "#f59e0b", // amber - key: "platforms", - gridPosition: "bottom-left" as const, - }, - "Talen & Frameworks": { - color: "#ef4444", // red - key: "frameworks", - gridPosition: "bottom-right" as const, - }, -} as const; - -type QuadrantName = keyof typeof QUADRANTS; - // Add numbers to items and group by quadrant const itemsWithNumbers: RadarItemWithNumber[] = items.map((item, index) => ({ ...item, @@ -104,8 +78,7 @@ const labelPositionMap: Record<string, { style: string; align: string }> = { data-quadrant={config.key} > <RadarQuadrant - gridPosition={config.gridPosition} - color={config.color} + quadrantName={name as QuadrantName} size={quadrantSize} items={itemsByQuadrant[name as QuadrantName]} parentPageSlug={parentPageSlug} @@ -552,27 +525,7 @@ const labelPositionMap: Record<string, { style: string; align: string }> = { </style> <script> - import { initializeOnReady } from "./radarUtils"; - - // Consolidated quadrant configuration - matches the Astro frontmatter - const QUADRANTS = { - Tools: { - color: "#10b981", - key: "tools", - }, - Technieken: { - color: "#3b82f6", - key: "techniques", - }, - Platformen: { - color: "#f59e0b", - key: "platforms", - }, - "Talen & Frameworks": { - color: "#ef4444", - key: "frameworks", - }, - } as const; + import { initializeOnReady, QUADRANTS } from "./radarUtils"; // Create reverse mapping: key -> name const quadrantNameMap: Record<string, string> = Object.fromEntries( diff --git a/libs/ui/src/radar/RadarQuadrant.astro b/libs/ui/src/radar/RadarQuadrant.astro index cb60fb8..8503212 100644 --- a/libs/ui/src/radar/RadarQuadrant.astro +++ b/libs/ui/src/radar/RadarQuadrant.astro @@ -1,23 +1,16 @@ --- import type { RadarRing, RadarItem } from "@xprtz/cms"; -import { buildRadarItemLink, RINGS } from "./radarUtils"; +import { buildRadarItemLink, RINGS, QUADRANTS, type QuadrantName, type GridPosition } from "./radarUtils"; interface ItemWithNumber extends RadarItem { number: number; } -type GridPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right"; - interface Props { /** - * Grid position of the quadrant + * Name of the quadrant */ - gridPosition: GridPosition; - - /** - * The color of the quadrant - */ - color: string; + quadrantName: QuadrantName; /** * Size of the quadrant in pixels @@ -35,7 +28,10 @@ interface Props { parentPageSlug?: string; } -const { gridPosition, color, size = 400, items = [], parentPageSlug } = Astro.props; +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 diff --git a/libs/ui/src/radar/radarUtils.ts b/libs/ui/src/radar/radarUtils.ts index e187766..02d028b 100644 --- a/libs/ui/src/radar/radarUtils.ts +++ b/libs/ui/src/radar/radarUtils.ts @@ -11,6 +11,39 @@ export const RINGS: readonly { readonly label: RadarRing; readonly radiusPositio { 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<string, { color: string; key: string; gridPosition: GridPosition }> = { + 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 From 1ed2fb89346e929d1a9df87a497d9165f2876ed7 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 16:13:01 +0100 Subject: [PATCH 33/44] added ring label gradient --- libs/ui/src/radar/RadarChart.astro | 44 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/libs/ui/src/radar/RadarChart.astro b/libs/ui/src/radar/RadarChart.astro index c5f4e39..b19ee59 100644 --- a/libs/ui/src/radar/RadarChart.astro +++ b/libs/ui/src/radar/RadarChart.astro @@ -56,6 +56,24 @@ const labelPositionMap: Record<string, { style: string; align: string }> = { "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, + }; +}); --- <div class="radar-chart-wrapper"> @@ -96,22 +114,16 @@ const labelPositionMap: Record<string, { style: string; align: string }> = { style={`top: ${quadrantSize}px; height: ${margin}px; width: ${quadrantSize}px;`} > { - 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; - const leftPosition = quadrantSize - ringCenter; - const labelStyle = `left: ${leftPosition}px; height: ${margin}px;`; + ringLabelData.map((data) => { + const leftPosition = quadrantSize - data.ringCenter; + const labelStyle = `left: ${leftPosition}px; height: ${margin}px; opacity: ${data.opacity};`; return ( <div class="absolute text-xs font-bold text-gray-600 flex items-center -translate-x-1/2" style={labelStyle} > - {ring.label} + {data.label} </div> ); }) @@ -125,21 +137,15 @@ const labelPositionMap: Record<string, { style: string; align: string }> = { style={`top: ${quadrantSize}px; height: ${margin}px; width: ${quadrantSize}px;`} > { - 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; - const labelStyle = `left: ${ringCenter}px; height: ${margin}px;`; + ringLabelData.map((data) => { + const labelStyle = `left: ${data.ringCenter}px; height: ${margin}px; opacity: ${data.opacity};`; return ( <div class="absolute text-xs font-bold text-gray-600 flex items-center -translate-x-1/2" style={labelStyle} > - {ring.label} + {data.label} </div> ); }) From 0d2da0feea6ce059b057797cc72c21a301e14610 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 16:32:09 +0100 Subject: [PATCH 34/44] tweaked hover states --- libs/ui/src/radar/RadarQuadrant.astro | 23 +++++++++++++++---- libs/ui/src/radar/RadarQuadrantItemList.astro | 14 ++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/libs/ui/src/radar/RadarQuadrant.astro b/libs/ui/src/radar/RadarQuadrant.astro index 8503212..41caa97 100644 --- a/libs/ui/src/radar/RadarQuadrant.astro +++ b/libs/ui/src/radar/RadarQuadrant.astro @@ -85,6 +85,8 @@ const generateQuarterCirclePath = (radiusPercent: number): string => { // 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 } => { @@ -134,9 +136,9 @@ const positionedItems = items.map((item, index) => { y = origin.y + radius * Math.sin(angle); } - // Clamp positions to keep circles within SVG bounds (considering circle radius) - x = Math.max(circleRadius, Math.min(size - circleRadius, x)); - y = Math.max(circleRadius, Math.min(size - circleRadius, y)); + // 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 }; }); @@ -201,7 +203,7 @@ const positionedItems = items.map((item, index) => { y={item.y + 1} text-anchor="middle" dominant-baseline="middle" - class="text-sm font-semibold pointer-events-none" + class="text-sm font-semibold pointer-events-none radar-item-text transition-all" fill={color} style="font-family: system-ui, -apple-system, sans-serif;" > @@ -223,10 +225,21 @@ const positionedItems = items.map((item, index) => { transition: all 0.2s ease; } + .radar-item-text { + transition: all 0.2s ease; + } + + /* Hover effect: invert colors - colored fill, white text and border */ .radar-item:hover .radar-item-circle, .radar-item.highlighted .radar-item-circle { - fill: color-mix(in srgb, var(--quadrant-color) 10%, white); + fill: var(--quadrant-color); + stroke: white; stroke-width: 3; r: 14; } + + .radar-item:hover .radar-item-text, + .radar-item.highlighted .radar-item-text { + fill: white; + } </style> diff --git a/libs/ui/src/radar/RadarQuadrantItemList.astro b/libs/ui/src/radar/RadarQuadrantItemList.astro index a54e633..d392cfa 100644 --- a/libs/ui/src/radar/RadarQuadrantItemList.astro +++ b/libs/ui/src/radar/RadarQuadrantItemList.astro @@ -76,7 +76,7 @@ const itemsByRing = RINGS.reduce( <h4 class="text-lg font-semibold text-gray-800 mb-2 sticky top-0 bg-white py-1"> {ring} </h4> - <ul class="space-y-2"> + <ul class="divide-y divide-gray-200"> {ringItems.map((item) => { const itemLink = buildRadarItemLink(item.slug, parentPageSlug); @@ -84,7 +84,8 @@ const itemsByRing = RINGS.reduce( <li> <a href={itemLink} - class="block p-3 rounded-md hover:bg-gray-50 transition-colors border border-gray-200 hover:border-gray-300" + class="item-link block py-3 transition-all" + style={`--item-color: ${color};`} > <div class="font-medium text-gray-900"> {item.number}. {item.title} @@ -113,7 +114,8 @@ const itemsByRing = RINGS.reduce( opacity: 0; transition: transform var(--slide-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1), - opacity var(--fade-out-duration, 300ms) cubic-bezier(0.4, 0, 0.2, 1) var(--fade-out-delay, 100ms); + opacity var(--fade-out-duration, 300ms) cubic-bezier(0.4, 0, 0.2, 1) + var(--fade-out-delay, 100ms); } .quadrant-item-list.hidden { @@ -135,6 +137,12 @@ const itemsByRing = RINGS.reduce( overflow: hidden; } + /* Item hover effect */ + .item-link:hover { + background-color: color-mix(in srgb, var(--item-color) 8%, white); + padding-left: 0.7rem; + } + /* Custom scrollbar styling */ .space-y-6::-webkit-scrollbar { width: 8px; From cf031cadaec6c532aadc2378d54f9d409222e55d Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 16:39:03 +0100 Subject: [PATCH 35/44] refactor duplicated styles --- libs/ui/src/radar/RadarChart.astro | 40 ++++++++++--------- libs/ui/src/radar/RadarQuadrantItemList.astro | 8 ++-- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/libs/ui/src/radar/RadarChart.astro b/libs/ui/src/radar/RadarChart.astro index b19ee59..62511ea 100644 --- a/libs/ui/src/radar/RadarChart.astro +++ b/libs/ui/src/radar/RadarChart.astro @@ -214,6 +214,10 @@ const ringLabelData = RINGS.map((ring, index) => { --fade-in-duration: 400ms; --zoom-duration: 500ms; --slide-duration: 400ms; + /* Reusable style values */ + --transition-easing: cubic-bezier(0.4, 0, 0.2, 1); + --zoom-scale: 1.75; + --hover-dim-opacity: 0.5; } .radar-chart-container { @@ -221,7 +225,7 @@ const ringLabelData = RINGS.map((ring, index) => { display: flex; justify-content: center; align-items: center; - transition: transform var(--slide-duration) cubic-bezier(0.4, 0, 0.2, 1); + transition: transform var(--slide-duration) var(--transition-easing); } .radar-chart-wrapper.list-visible .radar-chart-container { @@ -245,7 +249,7 @@ const ringLabelData = RINGS.map((ring, index) => { .radar-chart-wrapper.list-visible .radar-chart-container { opacity: 0; pointer-events: none; - transition: opacity var(--fade-out-duration) cubic-bezier(0.4, 0, 0.2, 1); + transition: opacity var(--fade-out-duration) var(--transition-easing); } .radar-chart-wrapper.list-animation-complete .radar-chart-container { @@ -290,7 +294,7 @@ const ringLabelData = RINGS.map((ring, index) => { .radar-chart-wrapper.list-visible .quadrant-rectangles { opacity: 0; pointer-events: none; - transition: opacity var(--fade-out-duration) cubic-bezier(0.4, 0, 0.2, 1); + transition: opacity var(--fade-out-duration) var(--transition-easing); } .radar-chart-wrapper.list-animation-complete .quadrant-rectangles { @@ -306,7 +310,7 @@ const ringLabelData = RINGS.map((ring, index) => { border-radius: 8px; padding: 20px; cursor: pointer; - transition: all var(--zoom-duration) cubic-bezier(0.4, 0, 0.2, 1); + transition: all var(--zoom-duration) var(--transition-easing); display: flex; align-items: center; justify-content: center; @@ -322,7 +326,7 @@ const ringLabelData = RINGS.map((ring, index) => { /* Hover dim effect for rectangles */ .quadrant-rectangles.hovering .quadrant-rect { - opacity: 0.5; + opacity: var(--hover-dim-opacity); } .quadrant-rectangles.hovering .quadrant-rect.active { @@ -438,7 +442,7 @@ const ringLabelData = RINGS.map((ring, index) => { } .radar-chart.hovering .quadrant-wrapper { - opacity: 0.5; + opacity: var(--hover-dim-opacity); } .radar-chart.hovering .quadrant-wrapper.active { @@ -446,7 +450,7 @@ const ringLabelData = RINGS.map((ring, index) => { } .radar-chart.hovering .quadrant-label { - opacity: 0.5; + opacity: var(--hover-dim-opacity); } .radar-chart.hovering .quadrant-label.active { @@ -456,7 +460,7 @@ const ringLabelData = RINGS.map((ring, index) => { /* Zoom functionality */ .quadrant-wrapper { cursor: pointer; - transition: all var(--zoom-duration) cubic-bezier(0.4, 0, 0.2, 1); + transition: all var(--zoom-duration) var(--transition-easing); transform-origin: center; position: relative; } @@ -469,24 +473,24 @@ const ringLabelData = RINGS.map((ring, index) => { .radar-chart.zoomed .quadrant-wrapper.zoomed-in { opacity: 1; pointer-events: auto; - transform: scale(1.75) translate(0, 0); + transform: scale(var(--zoom-scale)) translate(0, 0); } /* Position adjustments for each quadrant when zoomed */ .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="tools"] { - transform: scale(1.75) translate(25%, 25%); + transform: scale(var(--zoom-scale)) translate(25%, 25%); } .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="techniques"] { - transform: scale(1.75) translate(-25%, 25%); + transform: scale(var(--zoom-scale)) translate(-25%, 25%); } .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="platforms"] { - transform: scale(1.75) translate(25%, calc(-25% - 15px)); + transform: scale(var(--zoom-scale)) translate(25%, calc(-25% - 15px)); } .radar-chart.zoomed .quadrant-wrapper.zoomed-in[data-quadrant="frameworks"] { - transform: scale(1.75) translate(-25%, calc(-25% - 15px)); + transform: scale(var(--zoom-scale)) translate(-25%, calc(-25% - 15px)); } .radar-chart.zoomed .quadrant-label { @@ -495,7 +499,7 @@ const ringLabelData = RINGS.map((ring, index) => { /* Ring labels positioning and transitions */ .ring-labels { - transition: all var(--zoom-duration) cubic-bezier(0.4, 0, 0.2, 1); + transition: all var(--zoom-duration) var(--transition-easing); transform-origin: center; } @@ -508,25 +512,25 @@ const ringLabelData = RINGS.map((ring, index) => { /* Show and transform ring labels for Tools quadrant (top-left) */ .radar-chart.zoomed.zoom-tools .ring-labels[data-ring-labels="left"] { opacity: 1; - transform: scale(1.75) translate(25%, 190px); + transform: scale(var(--zoom-scale)) translate(25%, 190px); } /* Show and transform ring labels for Platforms quadrant (bottom-left) */ .radar-chart.zoomed.zoom-platforms .ring-labels[data-ring-labels="left"] { opacity: 1; - transform: scale(1.75) translate(25%, -205px); + transform: scale(var(--zoom-scale)) translate(25%, -205px); } /* Show and transform ring labels for Techniques quadrant (top-right) */ .radar-chart.zoomed.zoom-techniques .ring-labels[data-ring-labels="right"] { opacity: 1; - transform: scale(1.75) translate(-25%, 190px); + transform: scale(var(--zoom-scale)) translate(-25%, 190px); } /* Show and transform ring labels for Frameworks quadrant (bottom-right) */ .radar-chart.zoomed.zoom-frameworks .ring-labels[data-ring-labels="right"] { opacity: 1; - transform: scale(1.75) translate(-25%, -205px); + transform: scale(var(--zoom-scale)) translate(-25%, -205px); } </style> diff --git a/libs/ui/src/radar/RadarQuadrantItemList.astro b/libs/ui/src/radar/RadarQuadrantItemList.astro index d392cfa..5ed25da 100644 --- a/libs/ui/src/radar/RadarQuadrantItemList.astro +++ b/libs/ui/src/radar/RadarQuadrantItemList.astro @@ -113,8 +113,8 @@ const itemsByRing = RINGS.reduce( transform: translateX(100%); opacity: 0; transition: - transform var(--slide-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1), - opacity var(--fade-out-duration, 300ms) cubic-bezier(0.4, 0, 0.2, 1) + transform var(--slide-duration, 400ms) var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1)), + opacity var(--fade-out-duration, 300ms) var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1)) var(--fade-out-delay, 100ms); } @@ -126,8 +126,8 @@ const itemsByRing = RINGS.reduce( transform: translateX(0); opacity: 1; transition: - transform var(--slide-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1), - opacity var(--fade-in-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1); + transform var(--slide-duration, 400ms) var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1)), + opacity var(--fade-in-duration, 400ms) var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1)); } .line-clamp-2 { From c8aab1b592fa00575c3330cc4fdd255bac97b105 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 17:01:55 +0100 Subject: [PATCH 36/44] show filtering by tags also in item list --- libs/ui/src/radar/RadarQuadrantItemList.astro | 3 +- libs/ui/src/radar/TechnologyRadar.astro | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/libs/ui/src/radar/RadarQuadrantItemList.astro b/libs/ui/src/radar/RadarQuadrantItemList.astro index 5ed25da..cec41d2 100644 --- a/libs/ui/src/radar/RadarQuadrantItemList.astro +++ b/libs/ui/src/radar/RadarQuadrantItemList.astro @@ -79,9 +79,10 @@ const itemsByRing = RINGS.reduce( <ul class="divide-y divide-gray-200"> {ringItems.map((item) => { const itemLink = buildRadarItemLink(item.slug, parentPageSlug); + const tagsJson = JSON.stringify(item.tags?.map(t => t.title) || []); return ( - <li> + <li class="list-radar-item" data-tags={tagsJson}> <a href={itemLink} class="item-link block py-3 transition-all" diff --git a/libs/ui/src/radar/TechnologyRadar.astro b/libs/ui/src/radar/TechnologyRadar.astro index a5833da..5d352e9 100644 --- a/libs/ui/src/radar/TechnologyRadar.astro +++ b/libs/ui/src/radar/TechnologyRadar.astro @@ -105,21 +105,21 @@ const allTags = Array.from( }; const filterRadarItems = (tag: string | null) => { + // Filter both radar chart items and list items const radarItems = document.querySelectorAll(".radar-item"); + const listItems = document.querySelectorAll(".list-radar-item"); + // Filter radar chart items (dim and disable when not matching) radarItems.forEach((item) => { - // Get tags from data attribute const tagsAttr = item.getAttribute("data-tags"); const tags: string[] = tagsAttr ? JSON.parse(tagsAttr) : []; if (!tag) { - // Show all items item.setAttribute( "style", "transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;" ); } else { - // Check if item has the selected tag const hasTag = tags.includes(tag); if (hasTag) { item.setAttribute( @@ -134,6 +134,32 @@ const allTags = Array.from( } } }); + + // Filter list items (subtle dim but keep selectable) + listItems.forEach((item) => { + const tagsAttr = item.getAttribute("data-tags"); + const tags: string[] = tagsAttr ? JSON.parse(tagsAttr) : []; + + if (!tag) { + item.setAttribute( + "style", + "transition: opacity 0.3s ease; opacity: 1;" + ); + } else { + const hasTag = tags.includes(tag); + if (hasTag) { + item.setAttribute( + "style", + "transition: opacity 0.3s ease; opacity: 1;" + ); + } else { + item.setAttribute( + "style", + "transition: opacity 0.3s ease; opacity: 0.5;" + ); + } + } + }); }; // Initialize on page load From 2ebe603615618e1bf720e69b21a01de40fbfa04b Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 17:12:04 +0100 Subject: [PATCH 37/44] made tag filtering consistent between chart and list --- libs/ui/src/radar/RadarQuadrant.astro | 9 ++++ libs/ui/src/radar/RadarQuadrantItemList.astro | 9 ++++ libs/ui/src/radar/TechnologyRadar.astro | 51 +++---------------- 3 files changed, 24 insertions(+), 45 deletions(-) diff --git a/libs/ui/src/radar/RadarQuadrant.astro b/libs/ui/src/radar/RadarQuadrant.astro index 41caa97..2b39699 100644 --- a/libs/ui/src/radar/RadarQuadrant.astro +++ b/libs/ui/src/radar/RadarQuadrant.astro @@ -221,6 +221,10 @@ const positionedItems = items.map((item, index) => { display: block; } + .radar-item { + transition: opacity 0.3s ease; + } + .radar-item-circle { transition: all 0.2s ease; } @@ -229,6 +233,11 @@ const positionedItems = items.map((item, index) => { transition: all 0.2s ease; } + /* Filtered out state: dim but keep clickable */ + .radar-item.filtered-out { + opacity: 0.3; + } + /* Hover effect: invert colors - colored fill, white text and border */ .radar-item:hover .radar-item-circle, .radar-item.highlighted .radar-item-circle { diff --git a/libs/ui/src/radar/RadarQuadrantItemList.astro b/libs/ui/src/radar/RadarQuadrantItemList.astro index cec41d2..72df6b4 100644 --- a/libs/ui/src/radar/RadarQuadrantItemList.astro +++ b/libs/ui/src/radar/RadarQuadrantItemList.astro @@ -131,6 +131,15 @@ const itemsByRing = RINGS.reduce( opacity var(--fade-in-duration, 400ms) var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1)); } + .list-radar-item { + transition: opacity 0.3s ease; + } + + /* Filtered out state: dim but keep clickable */ + .list-radar-item.filtered-out { + opacity: 0.5; + } + .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; diff --git a/libs/ui/src/radar/TechnologyRadar.astro b/libs/ui/src/radar/TechnologyRadar.astro index 5d352e9..6d74b2f 100644 --- a/libs/ui/src/radar/TechnologyRadar.astro +++ b/libs/ui/src/radar/TechnologyRadar.astro @@ -108,56 +108,17 @@ const allTags = Array.from( // Filter both radar chart items and list items const radarItems = document.querySelectorAll(".radar-item"); const listItems = document.querySelectorAll(".list-radar-item"); + const allItems = [...Array.from(radarItems), ...Array.from(listItems)]; - // Filter radar chart items (dim and disable when not matching) - radarItems.forEach((item) => { + // Filter all items (dim but keep clickable) + allItems.forEach((item) => { const tagsAttr = item.getAttribute("data-tags"); const tags: string[] = tagsAttr ? JSON.parse(tagsAttr) : []; - if (!tag) { - item.setAttribute( - "style", - "transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;" - ); + if (!tag || tags.includes(tag)) { + item.classList.remove("filtered-out"); } else { - const hasTag = tags.includes(tag); - if (hasTag) { - item.setAttribute( - "style", - "transition: opacity 0.3s ease; opacity: 1; pointer-events: auto;" - ); - } else { - item.setAttribute( - "style", - "transition: opacity 0.3s ease; opacity: 0.2; pointer-events: none;" - ); - } - } - }); - - // Filter list items (subtle dim but keep selectable) - listItems.forEach((item) => { - const tagsAttr = item.getAttribute("data-tags"); - const tags: string[] = tagsAttr ? JSON.parse(tagsAttr) : []; - - if (!tag) { - item.setAttribute( - "style", - "transition: opacity 0.3s ease; opacity: 1;" - ); - } else { - const hasTag = tags.includes(tag); - if (hasTag) { - item.setAttribute( - "style", - "transition: opacity 0.3s ease; opacity: 1;" - ); - } else { - item.setAttribute( - "style", - "transition: opacity 0.3s ease; opacity: 0.5;" - ); - } + item.classList.add("filtered-out"); } }); }; From a753c6f7e340e44ec4dcedc4f3560d0d8cc0e69a Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 17:23:45 +0100 Subject: [PATCH 38/44] fix for hot-reloading css changes --- AGENTS.md | 24 ++++++++++++++++++++++++ apps/dotnet/astro.config.mjs | 13 +++++++++++++ apps/learning/astro.config.mjs | 12 ++++++++++++ 3 files changed, 49 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 57b6282..263e389 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,6 +162,30 @@ Provides shared TypeScript configuration for all workspaces with strict mode ena - Recent commits focus on Astro upgrades and feature additions - Deleted `pnpm-lock.yaml` (using npm instead of pnpm) +## 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. + ## Important Notes ### When Adding New Features diff --git a/apps/dotnet/astro.config.mjs b/apps/dotnet/astro.config.mjs index 8660643..af3870b 100644 --- a/apps/dotnet/astro.config.mjs +++ b/apps/dotnet/astro.config.mjs @@ -6,4 +6,17 @@ import react from "@astrojs/react"; export default defineConfig({ site: "https://xprtz.net", integrations: [tailwind(), sitemap(), react()], + vite: { + server: { + watch: { + // Force Vite to watch workspace dependencies + ignored: ['!**/node_modules/@xprtz/ui/**'] + } + }, + optimizeDeps: { + // Prevent pre-bundling of workspace dependencies + exclude: ['@xprtz/ui'] + } + } }); + diff --git a/apps/learning/astro.config.mjs b/apps/learning/astro.config.mjs index 90e481e..65fe4d9 100644 --- a/apps/learning/astro.config.mjs +++ b/apps/learning/astro.config.mjs @@ -7,4 +7,16 @@ export default defineConfig({ components: { 'Collapsible': './src/components/Collapsible.astro' }})], + vite: { + server: { + watch: { + // Force Vite to watch workspace dependencies + ignored: ['!**/node_modules/@xprtz/ui/**'] + } + }, + optimizeDeps: { + // Prevent pre-bundling of workspace dependencies + exclude: ['@xprtz/ui'] + } + } }); From e22f6300e40f5fc5a1f74667bd1309f8bbb548c6 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 21:33:54 +0100 Subject: [PATCH 39/44] refactoring radar item page --- apps/dotnet/src/pages/[slug].astro | 4 -- .../src/pages/radar-items/[radarItem].astro | 64 +++++++++---------- libs/ui/index.ts | 4 ++ 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/apps/dotnet/src/pages/[slug].astro b/apps/dotnet/src/pages/[slug].astro index 265c976..a462ba2 100644 --- a/apps/dotnet/src/pages/[slug].astro +++ b/apps/dotnet/src/pages/[slug].astro @@ -11,10 +11,6 @@ if (slug === "artikelen") { return Astro.rewrite("/artikelen/page/1"); } -// if (slug === "expertise") { -// return Astro.rewrite("/expertise/page/1"); -// } - export async function getStaticPaths() { const pageData = await fetchData<Array<PageType>>({ endpoint: "pages", diff --git a/apps/dotnet/src/pages/radar-items/[radarItem].astro b/apps/dotnet/src/pages/radar-items/[radarItem].astro index de85d73..832f081 100644 --- a/apps/dotnet/src/pages/radar-items/[radarItem].astro +++ b/apps/dotnet/src/pages/radar-items/[radarItem].astro @@ -1,7 +1,7 @@ --- import Layout from "../../layouts/layout.astro"; import { fetchData, type RadarItem } from "@xprtz/cms"; -import { Container } from "@xprtz/ui"; +import { Container, Text, QUADRANTS } from "@xprtz/ui"; export async function getStaticPaths() { const pageData = await fetchData<Array<RadarItem>>({ @@ -28,33 +28,9 @@ export async function getStaticPaths() { const radarItem: RadarItem = Astro.props; -// Get the parent page from query parameter (if provided) -const parentPageSlug = Astro.url.searchParams.get("from"); -const backLink = parentPageSlug ? `/${parentPageSlug}` : "/expertise"; - -// Define colors for each quadrant (matching RadarChart) -const quadrantColors = { - Techniques: "#3b82f6", // blue - Tools: "#10b981", // green - Platforms: "#f59e0b", // amber - "Languages & Frameworks": "#ef4444", // red -}; - -// Dutch translations for quadrant names -const quadrantTranslations = { - Techniques: "Technieken", - Tools: "Tools", - Platforms: "Platformen", - "Languages & Frameworks": "Talen & Frameworks", -}; - -const quadrantColor = - quadrantColors[radarItem.quadrant as keyof typeof quadrantColors] || - "#3b82f6"; -const quadrantDisplayName = - quadrantTranslations[ - radarItem.quadrant as keyof typeof quadrantTranslations - ] || radarItem.quadrant; +// Get quadrant configuration from QUADRANTS +const quadrantConfig = QUADRANTS[radarItem.quadrant]; +const quadrantColor = quadrantConfig?.color || "#3b82f6"; // Default: blue --- <Layout title={radarItem.title} description={radarItem.description}> @@ -119,7 +95,7 @@ const quadrantDisplayName = class="inline-flex items-center rounded-xl px-4 py-1 text-sm/6 font-medium" style={`background-color: ${quadrantColor}20; color: ${quadrantColor};`} > - {quadrantDisplayName} + {radarItem.quadrant} </span> </div> <div class="inline-flex"> @@ -145,9 +121,9 @@ const quadrantDisplayName = ) } - <p class="mt-6 text-lg/8"> - {radarItem.description} - </p> + {radarItem.description && ( + <Text content={radarItem.description} __component="ui.text" id={0} /> + )} { (radarItem.pros && radarItem.pros.length > 0) || @@ -260,14 +236,15 @@ const quadrantDisplayName = <h2 class="text-2xl font-bold tracking-tight text-gray-900"> Conclusie </h2> - <p class="mt-4 text-base/7">{radarItem.conclusion}</p> + <Text content={radarItem.conclusion} __component="ui.text" id={0} /> </div> ) } <div class="mt-10"> <a - href={backLink} + id="back-link" + href="/" class="text-base/7 font-semibold text-primary-600 hover:text-primary-500" > ← Terug naar radar @@ -279,3 +256,22 @@ const quadrantDisplayName = </div> </Container> </Layout> + +<script> + import { initializeOnReady } from "@xprtz/ui"; + + // Update back link based on query parameter (client-side only since pages are statically generated) + const updateBackLink = () => { + const backLink = document.getElementById("back-link") as HTMLAnchorElement; + if (backLink) { + const urlParams = new URLSearchParams(window.location.search); + const parentPageSlug = urlParams.get("from"); + if (parentPageSlug) { + backLink.href = `/${parentPageSlug}`; + } + } + }; + + // Initialize on page load + initializeOnReady(updateBackLink); +</script> diff --git a/libs/ui/index.ts b/libs/ui/index.ts index e28239d..7b4847b 100644 --- a/libs/ui/index.ts +++ b/libs/ui/index.ts @@ -23,6 +23,7 @@ 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"; @@ -52,5 +53,8 @@ export { RadarQuadrant, RadarChart, RadarQuadrantItemList, + QUADRANTS, + RINGS, + initializeOnReady, ComponentRenderer }; From 4b480a3324383716ce7f1e8d36db0526aee6619c Mon Sep 17 00:00:00 2001 From: Dick van Hirtum <dick@xprtz.net> Date: Mon, 29 Dec 2025 21:48:50 +0100 Subject: [PATCH 40/44] accessibility improvements --- libs/ui/src/Footer.astro | 2 +- libs/ui/src/radar/RadarQuadrant.astro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 && <a href={s.link} class="text-gray-400 hover:text-gray-500"> <span class="sr-only">{s.title}</span> - <img class="pt-4" src=`${imagesUrl}${s.icon.url}` /> + <img class="pt-4" src=`${imagesUrl}${s.icon.url}` alt={s.title} /> </a> ))} </div> diff --git a/libs/ui/src/radar/RadarQuadrant.astro b/libs/ui/src/radar/RadarQuadrant.astro index 2b39699..ea8180b 100644 --- a/libs/ui/src/radar/RadarQuadrant.astro +++ b/libs/ui/src/radar/RadarQuadrant.astro @@ -185,7 +185,7 @@ const positionedItems = items.map((item, index) => { return ( <g class="radar-item" style="transition: opacity 0.3s ease;" data-tags={tagsJson}> - <a href={itemLink}> + <a href={itemLink} aria-label={`${item.number}. ${item.title}`}> <title> {item.number}. {item.title} From 16e221707fbf56f18ddcde8c6f0b3eee26026902 Mon Sep 17 00:00:00 2001 From: Dick van Hirtum Date: Fri, 9 Jan 2026 09:48:11 +0100 Subject: [PATCH 41/44] renaming rectangles to tiles --- libs/ui/src/radar/RadarChart.astro | 144 ++++++++++++++++------------- 1 file changed, 78 insertions(+), 66 deletions(-) diff --git a/libs/ui/src/radar/RadarChart.astro b/libs/ui/src/radar/RadarChart.astro index 62511ea..3ae17c2 100644 --- a/libs/ui/src/radar/RadarChart.astro +++ b/libs/ui/src/radar/RadarChart.astro @@ -172,13 +172,13 @@ const ringLabelData = RINGS.map((ring, index) => {
- -