From e1953889e3c4675699295d097aeb451139e4eba8 Mon Sep 17 00:00:00 2001 From: Marius Kluften Date: Tue, 19 Nov 2024 12:35:19 +0100 Subject: [PATCH] =?UTF-8?q?Endre=20p=C3=A5=20areal=20for=20hver=20kvadrant?= =?UTF-8?q?=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kort oppsummert: Fikk ChatGPT til å hjelpe meg med å skrive om quadrant komponenten til å kunne: - Endre ratio for hvor mye plass hver dybde har i hver kvadrant. - Plassere blipsa i midten utover 3 rader, i stedet for 1 som blir for trang. Før: image Etter: image --- capra-fagradar/package.json | 3 +- capra-fagradar/src/radar/index.tsx | 657 +++++++++++------- capra-fagradar/src/radar/radar-store.ts | 16 + capra-fagradar/src/radar/radar.module.css | 7 +- .../src/tech-leader-radar/index.tsx | 36 +- capra-fagradar/src/technical-radar/index.tsx | 107 +-- pnpm-lock.yaml | 26 + 7 files changed, 527 insertions(+), 325 deletions(-) create mode 100644 capra-fagradar/src/radar/radar-store.ts diff --git a/capra-fagradar/package.json b/capra-fagradar/package.json index 5882390..85f9697 100644 --- a/capra-fagradar/package.json +++ b/capra-fagradar/package.json @@ -16,7 +16,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.0", - "react-tooltip": "^5.28.0" + "react-tooltip": "^5.28.0", + "zustand": "^5.0.1" }, "devDependencies": { "@mdx-js/rollup": "^3.0.1", diff --git a/capra-fagradar/src/radar/index.tsx b/capra-fagradar/src/radar/index.tsx index 01277d5..13c162f 100644 --- a/capra-fagradar/src/radar/index.tsx +++ b/capra-fagradar/src/radar/index.tsx @@ -1,16 +1,19 @@ -import React, { useState } from "react"; +import React from "react"; import { scaleLinear } from "d3-scale"; import styles from "./radar.module.css"; import "react-tooltip/dist/react-tooltip.css"; import { Tooltip } from "react-tooltip"; +import { useRadarStore } from "./radar-store"; export interface Blip { - id: number; + id: number; name: string; - logo?: string - depth: number; - is_new: boolean; + blipNumber: number; + logo?: string; + depth: number; + is_new: boolean; element: React.ReactElement; + quadrant: string; x: number; y: number; } @@ -20,73 +23,89 @@ type Color = string; // TODO: is there an html color / css color type? type QuadrantType = "top-left" | "top-right" | "bottom-left" | "bottom-right"; type BlipProps = { - blip: Blip; - color?: Color; - onClick: Function; + blip: Blip; + color?: Color; }; -const RadarBlip: React.FC = ({ blip, color, onClick }) => { - const className = styles.blip + (blip.is_new ? ` ${styles.circleOutline}` : ""); - - return ( -
-
onClick(e, blip)} - > - -
-
- ); +const RadarBlip: React.FC = ({ blip, color }) => { + const className = + styles.blip + (blip.is_new ? ` ${styles.circleOutline}` : ""); + + const { selectBlip, highlightBlip, highlightedBlip } = useRadarStore(); + + return ( +
+
highlightBlip(blip)} + onMouseLeave={() => highlightBlip(undefined)} + onClick={() => selectBlip(blip)} + > + {blip.blipNumber} +
+
+ ); }; type ArcProps = { orientation: QuadrantType; outerRadius: number; - size: number; + size: number; }; -const Arc : React.FC = ({ orientation, outerRadius, size }) => { - const getTransform = (orientation: QuadrantType, x: number, y: number) => { - switch (orientation) { - case "top-left": - return `rotate(-90, ${x}, ${y}) translate(0, ${size})`; - case "top-right": - return `rotate(0, ${x}, ${y})`; - case "bottom-left": - return `rotate(180, ${x}, ${y}) translate(-${size}, ${size})`; - case "bottom-right": - return `rotate(90, ${x}, ${y}) translate(-${size}, 0)`; - default: - return `rotate(0, ${x}, ${y})`; - } - }; - - const moveTo = (x: number, y: number) => `M ${x},${y}`; - const lineTo = (x: number, y: number) => `L ${x},${y}`; - const arcTo = ( - rx: number, - ry: number, - xAxisRotation: number, - largeArcFlag: number, - sweepFlag: number, - x: number, - y: number, - ) => `A ${rx},${ry} ${xAxisRotation} ${largeArcFlag},${sweepFlag} ${x},${y}`; - - const drawArc = (_orientation: QuadrantType, outerRadius: number) => { - const centerX = 0; - const centerY = size; - return ` +const Arc: React.FC = ({ orientation, outerRadius, size }) => { + const getTransform = (orientation: QuadrantType, x: number, y: number) => { + switch (orientation) { + case "top-left": + return `rotate(-90, ${x}, ${y}) translate(0, ${size})`; + case "top-right": + return `rotate(0, ${x}, ${y})`; + case "bottom-left": + return `rotate(180, ${x}, ${y}) translate(-${size}, ${size})`; + case "bottom-right": + return `rotate(90, ${x}, ${y}) translate(-${size}, 0)`; + default: + return `rotate(0, ${x}, ${y})`; + } + }; + + const moveTo = (x: number, y: number) => `M ${x},${y}`; + const lineTo = (x: number, y: number) => `L ${x},${y}`; + const arcTo = ( + rx: number, + ry: number, + xAxisRotation: number, + largeArcFlag: number, + sweepFlag: number, + x: number, + y: number, + ) => `A ${rx},${ry} ${xAxisRotation} ${largeArcFlag},${sweepFlag} ${x},${y}`; + + const drawArc = (_orientation: QuadrantType, outerRadius: number) => { + const centerX = 0; + const centerY = size; + return ` ${moveTo(centerX, centerY)} ${lineTo(centerX, centerY - outerRadius)} ${arcTo(outerRadius, outerRadius, 0, 0, 1, centerX + outerRadius, centerY)} Z `; - }; + }; return ( @@ -95,292 +114,404 @@ const Arc : React.FC = ({ orientation, outerRadius, size }) => { fill="white" stroke="#72777D" strokeWidth={1} - /> + /> - ) -} + ); +}; interface RadarChartProps { - name: string; - blipColor?: Color; - blips: Blip[]; - blipOnClick: Function; - orientation: QuadrantType; - maxDepth: number; - size: number; + name: string; + blipColor?: Color; + blips: Blip[]; + orientation: QuadrantType; + fractions: number[]; + maxDepth: number; + size: number; } const Quadrant: React.FC = ({ + fractions, name, - blipColor, - blips, - blipOnClick, - orientation, - maxDepth, - size, + blipColor, + blips, + orientation, + size, }) => { - const margin = 4 /* px */; - - const distributeBlips = ( - blips: Blip[], - depth: number, - size: number, - quadrant: QuadrantType, - maxDepth: number, - ) => { - /* - * Converts from degrees in range 0-360 to radians - */ - const degreeToRadians = (degrees: number) => (degrees / 360) * 2 * Math.PI; - - const angleMargin = degreeToRadians(20 / depth); - const angleScale = scaleLinear() - .domain([0, blips.length - 1]) - .range([0 + angleMargin, Math.PI / 2 - angleMargin]); - - return blips.map((blip, index) => { - const angle = angleScale(index); - const radius = ((depth - 0.5) * (size - margin)) / maxDepth; - - const x = radius * Math.cos(angle); - const y = radius * Math.sin(angle); - - switch (quadrant) { - case "top-left": - return { - ...blip, - x: size - x, - y: size - y, - }; - case "top-right": - return { - ...blip, - x: 0 + x, - y: size - y, - }; - case "bottom-left": - return { - ...blip, - x: size - x, - y: 0 + y, - }; - case "bottom-right": - return { - ...blip, - x: 0 + x, - y: 0 + y, - }; - } - }); - }; - - const groupedBlips = Object.groupBy( + const margin = 4; /* px */ + + const distributeBlips = ( + blips: Blip[], + depth: number, + size: number, + quadrant: QuadrantType, + ) => { + const degreeToRadians = (degrees: number) => (degrees / 360) * 2 * Math.PI; + + // Define the angle range for the quadrant + const angleStart = 0; + const angleEnd = Math.PI / 2; // 90 degrees in radians + + // Adjust radius calculation using custom fractions + const innerFraction = fractions[depth - 1]; + const outerFraction = fractions[depth]; + + const innerRadius = innerFraction * (size - margin); + const outerRadius = outerFraction * (size - margin); + + // Blip properties + const blipSize = 30; // blip size in pixels + const minSpacing = 5; // minimum spacing between blips in pixels + + if (depth === 1) { + // For depth === 1, arrange blips in two rows + // Calculate the radii for the two rows + const rowRadii = [ + innerRadius + (outerRadius - innerRadius) / 2.5, + innerRadius + (1.5 * outerRadius - innerRadius) / 2.5, + innerRadius + (2 * (outerRadius - innerRadius)) / 2.5, + ]; + + // Determine how many blips can fit in each row without overlapping + // Calculate the circumference for each row arc + const rowCircumferences = rowRadii.map( + (radius) => radius * (angleEnd - angleStart), + ); + + // Approximate number of blips that can fit in each row + const blipsPerRow = rowCircumferences.map((circumference) => + Math.floor(circumference / (blipSize + minSpacing)), + ); + + // Total blips that can be placed without overlapping + const totalCapacity = blipsPerRow.reduce((a, b) => a + b, 0); + + // If we have more blips than capacity, we need to adjust + const totalBlips = blips.length; + const blipsPerRowAdjusted = blipsPerRow.map((capacity) => + Math.floor((capacity / totalCapacity) * totalBlips), + ); + + // Adjust for any rounding errors + let adjustedTotal = blipsPerRowAdjusted.reduce((a, b) => a + b, 0); + let diff = totalBlips - adjustedTotal; + let rowIndex = 0; + while (diff > 0) { + blipsPerRowAdjusted[rowIndex % 2]++; + adjustedTotal++; + diff--; + rowIndex++; + } + + // Now distribute blips into the two rows + const blipsInRows: Array> = [[], [], []]; + let blipIndex = 0; + for (let i = 0; i < 3; i++) { + for (let j = 0; j < blipsPerRowAdjusted[i]; j++) { + blipsInRows[i].push(blips[blipIndex]); + blipIndex++; + } + } + + // Now compute positions for blips in each row + const positionedBlips = []; + for (let row = 0; row < 3; row++) { + const radius = rowRadii[row]; + const numBlipsInRow = blipsInRows[row].length; + const angleSpacing = (angleEnd - angleStart) / numBlipsInRow; + + for (let i = 0; i < numBlipsInRow; i++) { + const blip = blipsInRows[row][i]; + const angle = angleStart + angleSpacing * (i + 0.5); // Offset by 0.5 to center blips + + const x = radius * Math.cos(angle); + const y = radius * Math.sin(angle); + + let adjustedX, adjustedY; + switch (quadrant) { + case "top-left": + adjustedX = size - x; + adjustedY = size - y; + break; + case "top-right": + adjustedX = 0 + x; + adjustedY = size - y; + break; + case "bottom-left": + adjustedX = size - x; + adjustedY = 0 + y; + break; + case "bottom-right": + adjustedX = 0 + x; + adjustedY = 0 + y; + break; + } + + positionedBlips.push({ + ...blip, + x: adjustedX, + y: adjustedY, + }); + } + } + + return positionedBlips; + } else { + // For other depths, keep existing distribution logic + const angleMargin = degreeToRadians(20 / depth); + const angleScale = scaleLinear() + .domain([0, blips.length - 1]) + .range([angleStart + angleMargin, angleEnd - angleMargin]); + + return blips.map((blip, index) => { + const angle = angleScale(index); + const radius = (innerRadius + outerRadius) / 2; + + const x = radius * Math.cos(angle); + const y = radius * Math.sin(angle); + + let adjustedX, adjustedY; + switch (quadrant) { + case "top-left": + adjustedX = size - x; + adjustedY = size - y; + break; + case "top-right": + adjustedX = 0 + x; + adjustedY = size - y; + break; + case "bottom-left": + adjustedX = size - x; + adjustedY = 0 + y; + break; + case "bottom-right": + adjustedX = 0 + x; + adjustedY = 0 + y; + break; + } + + return { + ...blip, + x: adjustedX, + y: adjustedY, + }; + }); + } + }; + + const groupedBlips = Object.groupBy( blips, - ({ depth } : { depth : number }) => depth + ({ depth }: { depth: number }) => depth, ); - const distributedBlips = Object.keys(groupedBlips).map((depth : string) => { - return distributeBlips( - groupedBlips[Number(depth)] as any, - Number(depth), - size, - orientation, - maxDepth, - ); - }); - - const flattenedArrayBlips = Object.keys(distributedBlips) - .flatMap( - (depth : string) => distributedBlips[Number(depth)], + const distributedBlips = Object.keys(groupedBlips).map((depth: string) => { + return distributeBlips( + groupedBlips[Number(depth)] as any, + Number(depth), + size, + orientation, ); + }); + + const flattenedArrayBlips = Object.keys(distributedBlips).flatMap( + (depth: string) => distributedBlips[Number(depth)], + ); const quadrantSize = size - margin; - let arcs = Array(maxDepth + 1) - .fill(1) - .map((_itm, i) => i); - - // Draw the biggest arc first to have correct ordering (smaler arcs on top) - arcs.reverse(); - - return ( -
- - {arcs.map((i) => ( - + // Use custom cumulative fractions to define arcs + const arcs = fractions.slice(1).reverse(); + + return ( +
+ + {arcs.map((fraction, i) => ( + + ))} + + {(flattenedArrayBlips || []).map((blip, i) => ( + ))} - - {(flattenedArrayBlips || []).map((blip, i) => ( - - ))} - - {name} -
- ); + + + {name} + +
+ ); }; export type Quadrant = { - name: string; - orientation: QuadrantType; - blipColor: Color; - blips: Blip[]; + name: string; + orientation: QuadrantType; + blipColor: Color; + blips: Blip[]; }; -const RightAnchoredShelf: React.FC = ({ children }) => { - return ( -
- {children} -
- ); -} +const RightAnchoredShelf: React.FC = ({ + children, +}) => { + return
{children}
; +}; type LabelProps = React.PropsWithChildren; const Label: React.FC = ({ children }) => { - return ( -
{children}
- ); -} + return
{children}
; +}; type BlipInfoProps = { - blip: Blip; - onClose: React.MouseEventHandler; -} + blip: Blip; +}; -const BlipInfo: React.FC = ({ blip, onClose }) => { +const BlipInfo: React.FC = ({ blip }) => { + const { selectBlip } = useRadarStore(); return ( -

{blip.name}

+

+ {blip.name}{" "} + {blip.logo ? ( + + ) : null} +

+
- {blip.is_new && ()} + {blip.is_new && }
{blip.element}
- +
); -} - +}; interface QuadrantListProps { - name: string; - orientation: QuadrantType; - blips: Blip[]; - blipOnClick: Function; + name: string; + orientation: QuadrantType; + blips: Blip[]; } const QuadrantList: React.FC = ({ name, orientation, - blips, - blipOnClick, + blips, }) => { - const groupedBlips = Object.groupBy( + const groupedBlips = Object.groupBy( blips, - ({ depth } : { depth : number }) => depth + ({ depth }: { depth: number }) => depth, ); + const { selectBlip, highlightBlip, highlightedBlip } = useRadarStore(); return (

{name}

- { Object.keys(groupedBlips).map(depth => { - const blips = (groupedBlips[Number(depth)] || []) as Blip[]; - - return ( -
-

{ depth }

-
    - { blips.map((blip, i) => ( -
  • blipOnClick(e, blip)} - > - {blip.name} -
  • - ))} -
-
- ); - })} + {Object.keys(groupedBlips).map((depth) => { + const blips = (groupedBlips[Number(depth)] || []) as Blip[]; + + return ( +
+

{depth}

+
    + {blips.map((blip, i) => ( +
  • selectBlip(blip)} + onMouseEnter={() => highlightBlip(blip)} + onMouseLeave={() => highlightBlip(undefined)} + style={{ + listStyle: "none", + textDecoration: + blip.blipNumber === highlightedBlip?.blipNumber + ? "underline" + : "", + }} + > + {blip.blipNumber} - {blip.name} +
  • + ))} +
+
+ ); + })}
); -} +}; type Props = { - /* - * List of 4 quadrants - */ - quadrants: [Quadrant, Quadrant, Quadrant, Quadrant]; + /* + * List of 4 quadrants + */ + quadrants: [Quadrant, Quadrant, Quadrant, Quadrant]; + type: "tech-lead" | "technical"; }; -export const Radar: React.FC = ({ quadrants }) => { - const [currentBlip, setCurrentBlip] = useState(); +const leadFractions = [0, 0.4, 0.6, 0.8, 1]; +const techFractions = [0, 0.55, 0.7, 0.85, 1]; +export const Radar: React.FC = ({ quadrants, type }) => { + const { currentBlip } = useRadarStore(); - const maxDepth = 4; - const size = 480; + const maxDepth = 4; + const size = 480; - const blipOnClick = (_e : unknown, blip : Blip) => { - setCurrentBlip(blip); - } + const fractions = type === "tech-lead" ? leadFractions : techFractions; - return ( - <> + return ( + <>
- + + - - + + + + - + +
- { currentBlip && ( - setCurrentBlip(undefined)} /> - )} - - ); + {currentBlip && } + + ); }; diff --git a/capra-fagradar/src/radar/radar-store.ts b/capra-fagradar/src/radar/radar-store.ts new file mode 100644 index 0000000..12ed867 --- /dev/null +++ b/capra-fagradar/src/radar/radar-store.ts @@ -0,0 +1,16 @@ +import { create } from 'zustand' +import { Blip } from '.' + +type RadarState = { + currentBlip?: Blip + highlightedBlip?: Blip + selectBlip: (blip?: Blip) => void + highlightBlip: (blip?: Blip) => void +} + +export const useRadarStore = create((set) => ({ + currentBlip: undefined, + highlightedBlip: undefined, + selectBlip: (blip) => set({ currentBlip: blip }), + highlightBlip: (blip) => set({ highlightedBlip: blip }) +})) diff --git a/capra-fagradar/src/radar/radar.module.css b/capra-fagradar/src/radar/radar.module.css index def9b0d..302012d 100644 --- a/capra-fagradar/src/radar/radar.module.css +++ b/capra-fagradar/src/radar/radar.module.css @@ -1,6 +1,6 @@ .rightAnchoredShelf { position: fixed; - width: 45vw; + width: 25vw; top: 10rem; right: 0; background: white; @@ -9,6 +9,9 @@ padding: 2rem; border-radius: 1rem 0 0 1rem; z-index: 100; + p { + max-width: 50ch; + } } .quadrants { @@ -92,7 +95,7 @@ } .quadrantList { - font-size: 12px; + font-size: 0.9rem; h3 { text-transform: capitalize; diff --git a/capra-fagradar/src/tech-leader-radar/index.tsx b/capra-fagradar/src/tech-leader-radar/index.tsx index ffc4af2..851870c 100644 --- a/capra-fagradar/src/tech-leader-radar/index.tsx +++ b/capra-fagradar/src/tech-leader-radar/index.tsx @@ -1,9 +1,11 @@ import { createElement } from "react"; -import { Radar, type Quadrant } from "../radar"; +import { Blip, Radar, type Quadrant } from "../radar"; // Dynamically import all mdx files in current dir const modules = import.meta.glob("./**/*.mdx", { eager: true }) as any; -let items = []; +const unsortedBlips: Blip[] = []; + +let blipNumber = 1; for (const modulePath in modules) { const frontmatter = modules[modulePath]?.frontmatter; @@ -12,44 +14,54 @@ for (const modulePath in modules) { return
Empty
; }; - items.push({ + unsortedBlips.push({ ...frontmatter, element: createElement(defaultExport || Empty), }); } +const quadrants = ["Organisasjon", "Prosess", "Folk", "Teknologi"]; + +const blips = unsortedBlips + .sort((a, b) => (a.depth || 0) - (b.depth || 0)) + .filter((b) => quadrants.includes(b.quadrant)) + .map((blip) => ({ + ...blip, + blipNumber: blipNumber++, + })); + export const TechLeaderRadar = () => { const quadrants = [ { name: "Organisasjon", orientation: "top-left", - blipColor: "rgb(71, 161, 173)", - blips: [...items.filter((item) => item.quadrant === "Organisasjon")], + blipColor: "#47A1AD", + blips: [...blips.filter((item) => item.quadrant === "Organisasjon")], }, { name: "Teknologi", orientation: "top-right", - blipColor: "rgb(107, 158, 120)", - blips: [...items.filter((item) => item.quadrant === "Teknologi")], + blipColor: "#6B9E78", + blips: [...blips.filter((item) => item.quadrant === "Teknologi")], }, { name: "Folk", orientation: "bottom-left", - blipColor: "rgb(204, 133, 10)", - blips: [...items.filter((item) => item.quadrant === "Folk")], + blipColor: "#CC8508", + blips: [...blips.filter((item) => item.quadrant === "Folk")], }, { name: "Prosess eller arbeid?", orientation: "bottom-right", - blipColor: "rgb(225, 106, 124)", - blips: [...items.filter((item) => item.quadrant === "Prosess")], + blipColor: "#E16A7B", + blips: [...blips.filter((item) => item.quadrant === "Prosess")], }, ] satisfies [Quadrant, Quadrant, Quadrant, Quadrant]; return (

Teknologiledelse radar

- +
); }; diff --git a/capra-fagradar/src/technical-radar/index.tsx b/capra-fagradar/src/technical-radar/index.tsx index 6a0403e..a87732f 100644 --- a/capra-fagradar/src/technical-radar/index.tsx +++ b/capra-fagradar/src/technical-radar/index.tsx @@ -1,65 +1,78 @@ import { createElement } from 'react'; import { Radar, type Blip, type Quadrant } from '../radar'; +import type { ModuleNamespace } from 'vite/types/hot.js'; // Dynamically import all mdx files in current dir -const modules = import.meta.glob('./**/*.mdx', { eager: true }) as any; +const modules = import.meta.glob('./**/*.mdx', { eager: true }) as ModuleNamespace; const logos = import.meta.glob('./**/*.png', { eager: true }) as Record; -const blips: (Blip & any)[] = []; +const unsortedBlips: Blip[] = []; +let blipNumber = 1 for (const modulePath in modules) { - const frontmatter = modules[modulePath]?.frontmatter; - const defaultExport = modules[modulePath]?.default; - const logo = logos[modulePath.replace('.mdx', '.png')]?.default; - const Empty = () => { return
Empty
}; + const frontmatter = modules[modulePath]?.frontmatter; + const defaultExport = modules[modulePath]?.default; + const logo = logos[modulePath.replace('.mdx', '.png')]?.default; + const Empty = () => { return
Empty
}; - blips.push({ - ...frontmatter, - logo, - element: createElement(defaultExport || Empty), - }); + unsortedBlips.push({ + ...frontmatter, + logo, + element: createElement(defaultExport || Empty), + }); }; +const quadrants = ['backend', 'frontend', 'software_engineering', 'plattform'] + +const blips = unsortedBlips.sort((a, b) => + (a.depth || 0) - (b.depth || 0)) + .filter(b => quadrants.includes(b.quadrant)).map((blip) => ({ + ...blip, + blipNumber: blipNumber++ + })); + + + export const TechnicalRadar = () => { - const quadrants = [ - { - name: "backend", - orientation: "top-left", - blipColor: "rgb(71, 161, 173)", - blips: [ + const quadrants = [ + { + name: "backend", + orientation: "top-left", + blipColor: "#47A1AD", + blips: [ ...(blips.filter(item => item.quadrant === 'backend')), - ], - }, - { - name: "frontend", - orientation: "top-right", - blipColor: "rgb(107, 158, 120)", - blips: [ + ], + }, + { + name: "frontend", + orientation: "top-right", + blipColor: "#6A9D77", + blips: [ ...(blips.filter(item => item.quadrant === 'frontend')), - ], - }, - { - name: "software engineering", - orientation: "bottom-left", - blipColor: "rgb(204, 133, 10)", - blips: [ + ], + }, + { + name: "software engineering", + orientation: "bottom-left", + blipColor: "#CC8508", + blips: [ ...(blips.filter(item => item.quadrant === 'software_engineering')), - ], - }, - { - name: "plattform", - orientation: "bottom-right", - blipColor: "rgb(225, 106, 124)", - blips: [ + ], + }, + { + name: "plattform", + orientation: "bottom-right", + blipColor: "#DE6879", + blips: [ ...(blips.filter(item => item.quadrant === 'plattform')), - ], - }, - ] satisfies [Quadrant, Quadrant, Quadrant, Quadrant] ; + ], + }, + ] satisfies [Quadrant, Quadrant, Quadrant, Quadrant]; - return ( -
-

Teknisk radar

- -
- ); + return ( +
+

Teknisk radar

+ +
+ ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 604740b..d747470 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: react-tooltip: specifier: ^5.28.0 version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zustand: + specifier: ^5.0.1 + version: 5.0.1(@types/react@18.3.3)(react@18.3.1) devDependencies: '@mdx-js/rollup': specifier: ^3.0.1 @@ -970,6 +973,24 @@ packages: engines: {node: '>= 14'} hasBin: true + zustand@5.0.1: + resolution: {integrity: sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2160,4 +2181,9 @@ snapshots: yaml@2.5.0: {} + zustand@5.0.1(@types/react@18.3.3)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.3 + react: 18.3.1 + zwitch@2.0.4: {}