Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@
/>
<meta name="twitter:image" content="https://cadviewer.xyz/og-image.png" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link
rel="preload"
href="/fonts/ibm-plex-sans-400.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/ibm-plex-sans-500.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<script type="application/ld+json">
{
"@context": "https://schema.org",
Expand Down
Binary file added public/fonts/ibm-plex-mono-400.woff2
Binary file not shown.
Binary file added public/fonts/ibm-plex-mono-500.woff2
Binary file not shown.
Binary file added public/fonts/ibm-plex-sans-400.woff2
Binary file not shown.
Binary file added public/fonts/ibm-plex-sans-500.woff2
Binary file not shown.
Binary file added public/hero/3d-model-render.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/hero/generated-drawing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 89 additions & 0 deletions src/components/Accordion.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
.item {
position: relative;
background: var(--color-white);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 0 1.5rem;
transition: border-color 0.14s ease-out;
}

.item:hover,
.item:focus-within {
border-color: var(--color-blue-500);
}

.trigger {
width: 100%;
margin: 0;
padding: 1rem 0;
background: none;
border: none;
color: var(--color-ink);
font-family: var(--font-display);
font-size: 1rem;
font-weight: 500;
text-align: left;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}

.trigger:focus-visible {
outline: 2px solid var(--color-blue-500);
outline-offset: 2px;
}

.chevron {
flex: none;
color: var(--color-blue-500);
transition: transform 0.2s ease;
}

.chevronOpen {
transform: rotate(180deg);
}

.plusIcon {
flex: none;
display: inline-block;
color: var(--color-blue-500);
font-size: 1.2rem;
line-height: 1;
transition: transform 0.2s ease;
}

.plusIconOpen {
transform: rotate(45deg);
}

.contentWrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.2s ease;
}

.contentOpen {
grid-template-rows: 1fr;
}

.contentInner {
overflow: hidden;
min-height: 0;
}

.answer {
margin: 0 0 1.25rem;
color: var(--color-ink-muted);
font-size: 0.95rem;
line-height: 1.65;
}

@media (prefers-reduced-motion: reduce) {
.contentWrap,
.chevron,
.plusIcon {
transition: none;
}
}
77 changes: 77 additions & 0 deletions src/components/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Shared FAQ accordion entry, used on Landing and both guide pages so the
// open/close interaction (height transition + chevron rotation) is
// identical everywhere instead of three copies of native <details> markup.
// Each item opens/closes independently — no shared "only one open" state.
import { useState, type ReactNode } from "react";
import styles from "./Accordion.module.css";

export default function AccordionItem({
question,
answer,
className,
icon = "chevron",
children,
}: {
question: string;
answer: ReactNode;
className?: string;
/** "chevron" (default, used on the guide pages) rotates 180deg on open.
* "plus" (used on Landing) turns a + into a x at 45deg. */
icon?: "chevron" | "plus";
children?: ReactNode;
}) {
const [open, setOpen] = useState(false);

return (
<div className={[styles.item, className].filter(Boolean).join(" ")}>
{children}
<button
type="button"
className={styles.trigger}
aria-expanded={open}
onClick={() => setOpen((prev) => !prev)}
>
<span>{question}</span>
{icon === "plus" ? (
<span
className={[styles.plusIcon, open ? styles.plusIconOpen : ""]
.filter(Boolean)
.join(" ")}
aria-hidden="true"
>
+
</span>
) : (
<svg
className={[styles.chevron, open ? styles.chevronOpen : ""]
.filter(Boolean)
.join(" ")}
viewBox="0 0 16 16"
width="14"
height="14"
aria-hidden="true"
focusable="false"
>
<path
d="M4 6 L8 10 L12 6"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</button>
<div
className={[styles.contentWrap, open ? styles.contentOpen : ""]
.filter(Boolean)
.join(" ")}
>
<div className={styles.contentInner}>
<p className={styles.answer}>{answer}</p>
</div>
</div>
</div>
);
}
51 changes: 51 additions & 0 deletions src/components/CornerMark.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.mark {
display: block;
overflow: visible;
}

.stroke {
fill: none;
stroke: var(--color-blue-500);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}

.drawIn .stroke {
/* Both strokes in the mark are constructed to the same length (14 user
* units); 16 is a safe rounded-up dash length that fully hides the
* stroke before the draw-in animation starts. */
stroke-dasharray: 16;
stroke-dashoffset: 16;
animation: corner-draw-in 1.1s ease-out forwards;
animation-delay: var(--corner-mark-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
.drawIn .stroke {
animation: none;
stroke-dashoffset: 0;
}
}

@keyframes corner-draw-in {
to {
stroke-dashoffset: 0;
}
}

/* Hover "sharpen" state — applied by a parent card via .sharpen on hover/focus.
* Opacity/scale only, quick and precise, no bounce. */
.mark {
opacity: 0.55;
transform: scale(0.94);
transition:
opacity 0.14s ease-out,
transform 0.14s ease-out;
}

:global(.cornerMarkSharpen):hover .mark,
:global(.cornerMarkSharpen):focus-within .mark {
opacity: 1;
transform: scale(1);
}
65 changes: 65 additions & 0 deletions src/components/CornerMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { CSSProperties } from "react";
import styles from "./CornerMark.module.css";

export type CornerMarkCorner =
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right";

// Registration-mark style bracket: two strokes crossing at the corner
// point, each overshooting slightly past the corner (into the margin)
// the way the ISO centring marks on the generated drawing sheet cross
// past the frame line — see GRID_REF_CENTRING_MARK_CROSS_MM in
// drafting-rules.ts. This is a separate, decorative echo of that idea,
// not a reproduction of the drawing sheet's own marks.
const PATHS: Record<CornerMarkCorner, { h: string; v: string }> = {
"top-left": { h: "M2,6 H16", v: "M6,2 V16" },
"top-right": { h: "M22,6 H8", v: "M18,2 V16" },
"bottom-left": { h: "M2,18 H16", v: "M6,22 V8" },
"bottom-right": { h: "M22,18 H8", v: "M18,22 V8" },
};

const POSITION_STYLE: Record<CornerMarkCorner, CSSProperties> = {
"top-left": { top: 0, left: 0 },
"top-right": { top: 0, right: 0 },
"bottom-left": { bottom: 0, left: 0 },
"bottom-right": { bottom: 0, right: 0 },
};

export default function CornerMark({
size = 24,
corner,
animate = false,
delay = 0,
className,
}: {
size?: number;
corner: CornerMarkCorner;
animate?: boolean;
delay?: number;
className?: string;
}) {
const paths = PATHS[corner];

return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
aria-hidden="true"
focusable="false"
className={[styles.mark, animate ? styles.drawIn : "", className]
.filter(Boolean)
.join(" ")}
style={{
position: "absolute",
...POSITION_STYLE[corner],
["--corner-mark-delay" as string]: `${delay}ms`,
}}
>
<path className={styles.stroke} d={paths.h} />
<path className={styles.stroke} d={paths.v} />
</svg>
);
}
19 changes: 19 additions & 0 deletions src/components/Reveal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.reveal {
opacity: 0;
transform: translateY(var(--reveal-distance, 8px));
transition:
opacity var(--reveal-duration, 0.4s) ease-out,
transform var(--reveal-duration, 0.4s) ease-out;
transition-delay: var(--reveal-delay, 0ms);
}

.reveal.revealed {
opacity: 1;
transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
.reveal {
transition: none;
}
}
Loading