Skip to content
6

Feature 8

Six-card feature grid on hairline rails: each card carries an icon tile, a title and a description, corner crosshair marks, and a radial spotlight that follows the pointer on hover. Warm-tinted top glow per card.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/feature-08.json

Code

components/blocks/feature-08.tsx
'use client';

import { Activity, FileCode2, KeyRound, Terminal, Workflow, Zap } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Badge } from '@/registry/hirael/bases/radix/ui/badge';

const FEATURES = [
  {
    icon: Workflow,
    title: 'A canvas, not a config file',
    description:
      'Drag steps onto a graph, wire dependencies by hand, and let the layout show fan-out and gates for you.',
  },
  {
    icon: FileCode2,
    title: 'Two-way sync',
    description: 'Edit the graph or the file, the other follows. The config in your repo stays the source of truth.',
  },
  {
    icon: Activity,
    title: 'Runs you can watch',
    description:
      'Every run replays on the same graph. The failing step turns red where it sits, with its logs one click away.',
  },
  {
    icon: KeyRound,
    title: 'Secrets per environment',
    description: 'Scope credentials to an environment, not a pipeline. Staging never sees production keys.',
  },
  {
    icon: Zap,
    title: 'Retries that target the step',
    description: 'Rerun a failed step in place with its cache intact instead of paying for the whole pipeline again.',
  },
  {
    icon: Terminal,
    title: 'API and CLI',
    description: 'Trigger runs, read statuses and export graphs from scripts. Everything the UI does has an endpoint.',
  },
];

/** Corner crosshair, drawn half outside the card edge like a survey mark. */
const CrossDecor = ({ position }: { position: 'top-start' | 'bottom-end' }) => {
  return (
    <svg
      aria-hidden
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1"
      strokeLinecap="round"
      className={cn(
        'pointer-events-none absolute z-10 size-3.5 shrink-0 text-muted-foreground',
        position === 'top-start' && 'start-0 top-0 -translate-y-1/2 ltr:-translate-x-1/2 rtl:translate-x-1/2',
        position === 'bottom-end' && 'bottom-0 end-0 translate-y-1/2 ltr:translate-x-1/2 rtl:-translate-x-1/2',
      )}
    >
      <path d="M5 12h14" />
      <path d="M12 5v14" />
    </svg>
  );
};

const FeatureCard = ({ className, children, ...props }: React.ComponentProps<'div'>) => {
  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    e.currentTarget.style.setProperty('--mx', `${e.clientX - rect.left}px`);
    e.currentTarget.style.setProperty('--my', `${e.clientY - rect.top}px`);
  };

  return (
    <div
      onPointerMove={handlePointerMove}
      data-slot="feature-card"
      className={cn(
        'group relative flex h-full flex-col justify-start gap-6 bg-background px-6 pb-6 pt-8 shadow-xs',
        'bg-[radial-gradient(50%_80%_at_25%_0%,var(--warm-glow),transparent)]',
        className,
      )}
      {...props}
    >
      {/* Pointer-follow spotlight, fades in on hover along --mx/--my. */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100 bg-[radial-gradient(circle_200px_at_var(--mx)_var(--my),color-mix(in_oklch,var(--warm)_14%,transparent),transparent_70%)]"
      />

      <div className="absolute -inset-y-4 -start-px w-px bg-border" />
      <div className="absolute -inset-y-4 -end-px w-px bg-border" />
      <div className="absolute -inset-x-4 -top-px h-px bg-border" />
      <div className="absolute -inset-x-4 -bottom-px h-px bg-border" />

      {children}
    </div>
  );
};

const Feature08 = () => {
  return (
    <section
      data-slot="feature"
      className="flex w-full flex-col justify-center gap-12 bg-background px-6 py-16 md:px-10 md:py-24"
    >
      <div data-slot="feature-header" className="mx-auto flex max-w-3xl flex-col items-center gap-4 text-center">
        <Badge
          variant="outline"
          className="rounded-full bg-card/70 px-4 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
        >
          Features
        </Badge>
        <h2 className="font-serif text-3xl font-medium tracking-tight md:text-4xl lg:text-5xl">
          Everything a pipeline needs
        </h2>
        <p className="text-base leading-relaxed text-muted-foreground md:text-lg">
          The editor is the headline, but the platform underneath carries the boring parts: secrets, caching, retries
          and an API for the rest.
        </p>
      </div>

      <div className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
        {FEATURES.map((feature) => (
          <FeatureCard key={feature.title}>
            <CrossDecor position="top-start" />
            <CrossDecor position="bottom-end" />
            <div
              data-slot="feature-card-icon"
              className="relative z-10 flex w-fit items-center justify-center rounded-xs border border-border bg-muted/20 p-3 transition-colors duration-300 group-hover:border-warm/40"
            >
              <feature.icon aria-hidden className="size-5 stroke-[1.5] text-foreground" />
            </div>
            <div className="relative z-10 flex flex-col gap-2">
              <h3 className="text-base font-medium text-foreground">{feature.title}</h3>
              <p className="text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
            </div>
          </FeatureCard>
        ))}
      </div>
    </section>
  );
};

export default Feature08;

Dependencies

shadcn registry

badge

npm

lucide-react

Shipped