Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/sorcerer/llms.txt

Use this file to discover all available pages before exploring further.

The Skills page — headed “Familiar Skills” with the subtitle “The constellations of my craft” — renders the developer’s technology stack as an interactive star map. Nine skill nodes are scattered across a container at fixed percentage coordinates, connected by eleven SVG lines that animate from zero length to full length when the section scrolls into view. Each node is a silver dot with a label beneath it; hovering over a node reveals a turquoise glow halo. The whole graph sits on a dotted grid background that reinforces the astronomical theme.

The Constellation

Node and Edge System

Each node represents a single skill. It is positioned absolutely within a relative container using left and top CSS properties expressed as percentages, so the constellation scales with the viewport without any JavaScript resizing logic. The visible mark is a 12×12 px silver dot (bg-slate-200 rounded-full) with the skill name rendered in small text directly below it. On hover, a turquoise ring (ring-2 ring-teal-400) expands around the dot to create the glow halo effect. Edges are <line> elements drawn inside a full-size <svg> that is absolutely positioned behind all the node dots. Each line’s x1 / y1 / x2 / y2 coordinates are derived from the x and y percentage values of the two nodes it connects, converted to pixel positions at render time so the SVG geometry always matches the DOM layout.

Dotted Grid Background

The containing element uses an inline CSS backgroundImage to produce the star-chart grid:
style={{
  backgroundImage:
    "radial-gradient(#e2e8f0 1px, transparent 1px)",
  backgroundSize: "40px 40px",
}}
This draws a 1 px silver dot at every 40 px interval across the full container, creating a subtle lattice that looks like a field of background stars without adding any DOM nodes.

Skills Data

The two data structures that drive the constellation are the nodes array and the edges array, both defined inside the SkillsPage component:
const nodes = [
  { id: "react",    label: "React",      x: 20, y: 30 },
  { id: "ts",       label: "TypeScript", x: 40, y: 20 },
  { id: "node",     label: "Node.js",    x: 60, y: 35 },
  { id: "python",   label: "Python",     x: 80, y: 25 },
  { id: "sql",      label: "SQL",        x: 70, y: 60 },
  { id: "graphql",  label: "GraphQL",    x: 50, y: 50 },
  { id: "aws",      label: "AWS",        x: 30, y: 65 },
  { id: "docker",   label: "Docker",     x: 45, y: 80 },
  { id: "css",      label: "Tailwind",   x: 15, y: 55 },
];

const edges = [
  ["react",   "ts"],
  ["ts",      "node"],
  ["node",    "python"],
  ["node",    "sql"],
  ["sql",     "graphql"],
  ["graphql", "ts"],
  ["graphql", "react"],
  ["aws",     "docker"],
  ["node",    "aws"],
  ["react",   "css"],
  ["css",     "ts"],
];
x and y are unitless numbers that the component appends a % sign to when writing CSS. An x of 20 therefore means left: 20% of the container width.

Animation

useInView Trigger

The entire SVG edges group is wrapped in a Framer Motion motion.g controlled by a useInView hook. The hook watches a ref attached to the container and fires once — once: true — with a root margin of -100px so the animation does not start until the graph is 100 px inside the viewport:
import { useInView } from "framer-motion";

const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });

SVG Line Draw-on Animation

Each <line> element is replaced with a <motion.line> that animates its pathLength from 0 to 1. Because SVG <line> elements do not natively support pathLength, the implementation uses strokeDasharray and strokeDashoffset under the hood — Framer Motion handles this automatically when you pass pathLength as a motion value:
{edges.map(([from, to], i) => (
  <motion.line
    key={`${from}-${to}`}
    x1={`${nodeMap[from].x}%`}
    y1={`${nodeMap[from].y}%`}
    x2={`${nodeMap[to].x}%`}
    y2={`${nodeMap[to].y}%`}
    stroke="#475569"
    strokeWidth={1}
    initial={{ pathLength: 0, opacity: 0 }}
    animate={isInView ? { pathLength: 1, opacity: 1 } : {}}
    transition={{
      pathLength: { duration: 1.5, delay: i * 0.2, ease: "easeInOut" },
      opacity:    { duration: 0.3, delay: i * 0.2 },
    }}
  />
))}
PropertyValueEffect
pathLength0 → 1Line draws from start node to end node
duration1.5 sEach edge takes 1.5 s to fully appear
delayi × 0.2Edges stagger: edge 0 at 0 s, edge 10 at 2 s
oncetrueAnimation plays once and stays in final state

Node Spring Scale

Each skill dot scales in with a spring transition after the edges begin drawing, reinforcing the sense that the constellation is assembling itself:
<motion.div
  initial={{ scale: 0 }}
  animate={isInView ? { scale: 1 } : {}}
  transition={{ type: "spring", stiffness: 300, damping: 20, delay: i * 0.1 }}
  className="w-3 h-3 rounded-full bg-slate-200"
/>

Adding Skills

1

Add a new node to the nodes array

Open the SkillsPage component, locate the nodes array, and append a new object. Choose an id that is unique across all existing nodes, a human-readable label, and x / y values that place the node in an unoccupied area of the graph:
{ id: "rust", label: "Rust", x: 65, y: 75 }
2

Add edges connecting the new node

In the edges array just below nodes, add one or more two-element arrays that pair your new node’s id with the id of an existing node. Edges are undirected — ["rust", "node"] and ["node", "rust"] are equivalent:
["rust", "node"],
["rust", "docker"],
3

Verify x and y are within bounds

The x and y values are percentages of the constellation container’s width and height respectively. Keep both values between 5 and 92 to avoid nodes being clipped by the container edge or overlapping the label of an adjacent node. The dotted grid spans the full container, so any position within those bounds will look natural against the background.

Build docs developers (and LLMs) love