diff --git a/index.html b/index.html index 7cc676c..066d3ba 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,20 @@ /> + + +
- { - const file = e.target.files?.[0]; - if (file) { - setPendingFile(file); - navigate("/viewer"); - } - }} - /> + - {/* LAYER 0: animated canvas — must be first child */} - - - {/* LAYER 1: soft fog — dims the grid/scan behind the hero content, */} - {/* stays visible in the empty margins around it */} -
+
+
- -
-
-

Why it’s free

-

- Every file you load is processed entirely in your browser — - nothing is uploaded to a server. That’s not a privacy - feature bolted on top; it’s the whole architecture, built - on OpenCascade compiled to WebAssembly. Because nothing runs - server-side, there’s no hosting cost that scales with - usage, which is what makes it possible to keep this free rather - than metered or subscription-gated. -

-
-
- -
-
-

- Frequently asked questions -

-
- {FAQS.map((faq) => ( -
- - {faq.question} - -

{faq.answer}

-
+
+ +
+ + + +
+ +
+

How it works

+
+ {STEPS.map((step, index) => ( + + 0{index + 1} +

{step.title}

+

{step.description}

+
))}
-
- - - + + +
+

What you can do with it

+ + + + + +
+

View any CAD format, instantly.

+

+ STEP, IGES, STL, OBJ, 3MF, BREP, and GLB open directly in the + browser. Orbit, pan, section, and measure edges — no plugin, + no viewer app to install separately. +

+
+
+ +
+
+ PDF export + + + +

+ Generate a real 2D engineering drawing. +

+

+ Click Generate 2D Drawing and get Front, Top, and Right + orthographic views in third-angle projection, an isometric + reference view, and automatic dimensioning — hole sizes, + locations, and overall dimensions placed per standard + drafting convention, not just wherever fits visually. Export + as a true-to-scale PDF: printed at 100%, the dimensions on + the page match the real part. +

+
+
+ + + +

Work with assemblies.

+

+ Explode and disassemble multi-part files, and export any + individual part as its own STEP file. +

+
+
+ + + +

Measure what matters.

+

+ Click-to-measure edges, apply section planes, and compare + scale against common reference objects. +

+
+
+
+ +
+ +
+

Why it’s free

+

+ Every file you load is processed entirely in your browser — + nothing is uploaded to a server. That’s not a privacy + feature bolted on top; it’s the whole architecture, + built on OpenCascade compiled to WebAssembly. Because nothing + runs server-side, there’s no hosting cost that scales + with usage, which is what makes it possible to keep this + free rather than metered or subscription-gated. +

+
+ +
+
+ +
+ +

Frequently asked questions

+
+ {FAQS.map((faq) => ( + + ))} +
+
+
+ + +
); } diff --git a/src/pages/TriangleMark.tsx b/src/pages/TriangleMark.tsx index d863a4f..3381fac 100644 --- a/src/pages/TriangleMark.tsx +++ b/src/pages/TriangleMark.tsx @@ -12,7 +12,7 @@ export default function TriangleMark({ size = 108 }: { size?: number }) { diff --git a/src/pages/guides/SplitStepAssembly.module.css b/src/pages/guides/SplitStepAssembly.module.css index 3695378..702dfa4 100644 --- a/src/pages/guides/SplitStepAssembly.module.css +++ b/src/pages/guides/SplitStepAssembly.module.css @@ -2,6 +2,7 @@ overflow-y: auto; height: auto; min-height: 100vh; + font-family: var(--font-display); } :global(body.guide-page #root) { @@ -11,11 +12,11 @@ .page { min-height: 100vh; - background: #fafafa; - color: #1a1a1a; + background: var(--color-bg); + color: var(--color-ink); display: flex; justify-content: center; - padding: 4rem 1.5rem 6rem; + padding: 4rem 1.5rem 0; } .article { @@ -23,9 +24,11 @@ display: flex; flex-direction: column; gap: clamp(3rem, 5vw, 4rem); + padding-bottom: clamp(3rem, 5vw, 4rem); } .section { + position: relative; display: flex; flex-direction: column; gap: 1.25rem; @@ -34,21 +37,24 @@ .breadcrumb { display: inline-flex; align-self: flex-start; - color: #4b5563; + color: var(--color-ink-muted); font-size: 0.9rem; text-decoration: none; } .breadcrumb:hover { - color: #3b82f6; + color: var(--color-blue-500); +} + +.breadcrumb:focus-visible { + outline: 2px solid var(--color-blue-500); + outline-offset: 2px; } .eyebrow { margin: 0; - color: #6b7280; - font-family: - "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; + color: var(--color-ink-faint); + font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; @@ -56,36 +62,36 @@ .headline { margin: 0.5rem 0 0; - color: #1a1a1a; + color: var(--color-ink); font-size: 2.25rem; line-height: 1.2; - font-weight: 700; + font-weight: 500; } .intro { margin: 0; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1.1rem; line-height: 1.7; } .lede { margin: 0; - color: #1f2937; + color: var(--color-ink); font-size: 1.1rem; line-height: 1.7; } .sectionHeading { margin: 0; - color: #1a1a1a; + color: var(--color-ink); font-size: 1.5rem; - font-weight: 700; + font-weight: 500; } .paragraph { margin: 0; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1.05rem; line-height: 1.7; } @@ -93,7 +99,7 @@ .list { margin: 0; padding-left: 1.25rem; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1.05rem; line-height: 1.7; display: flex; @@ -102,13 +108,18 @@ } .externalLink { - color: #2563eb; + color: var(--color-blue-600); text-decoration: underline; text-underline-offset: 2px; } .externalLink:hover { - color: #1d4ed8; + color: var(--color-blue-700); +} + +.externalLink:focus-visible { + outline: 2px solid var(--color-blue-500); + outline-offset: 2px; } /* "Why you'd need to do this" cards */ @@ -122,13 +133,20 @@ } .reasonCard { - background: #f3f4f6; - border: 1px solid #e5e7eb; + position: relative; + background: var(--color-bg-alt); + border: 1px solid var(--color-border); border-radius: 12px; padding: 1.1rem 1.4rem; - color: #374151; + color: var(--color-ink-muted); font-size: 1.02rem; line-height: 1.6; + transition: border-color 0.14s ease-out; +} + +.reasonCard:hover, +.reasonCard:focus-within { + border-color: var(--color-blue-500); } /* Numbered step cards */ @@ -142,13 +160,20 @@ } .stepCard { + position: relative; display: flex; align-items: flex-start; gap: 1.1rem; - background: #ffffff; - border: 1px solid #e5e7eb; + background: var(--color-white); + border: 1px solid var(--color-border); border-radius: 12px; padding: 1.25rem 1.5rem; + transition: border-color 0.14s ease-out; +} + +.stepCard:hover, +.stepCard:focus-within { + border-color: var(--color-blue-500); } .stepNumber { @@ -159,10 +184,11 @@ width: 2.5rem; height: 2.5rem; border-radius: 999px; - background: #eff6ff; - color: #3b82f6; + background: var(--color-blue-50); + color: var(--color-blue-500); + font-family: var(--font-mono); font-size: 1.15rem; - font-weight: 700; + font-weight: 500; } .stepBody { @@ -173,19 +199,20 @@ .stepTitle { margin: 0; - color: #1a1a1a; + color: var(--color-ink); font-size: 1.05rem; - font-weight: 700; + font-weight: 500; } .stepDescription { margin: 0; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1rem; line-height: 1.6; } .stepImageWrap { + position: relative; margin: 0.5rem 0; } @@ -196,83 +223,65 @@ gap: 0.75rem; } -.faqItem { - background: #ffffff; - border: 1px solid #e5e7eb; - border-radius: 12px; - padding: 0.35rem 1.5rem; -} - -.faqItem[open] { - padding-bottom: 1.25rem; -} - -.faqQuestion { - margin: 0; - padding: 1rem 0; - color: #1a1a1a; - font-size: 1.05rem; - font-weight: 600; - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.faqQuestion::-webkit-details-marker { - display: none; -} - -.faqQuestion::after { - content: "+"; - flex: none; - color: #3b82f6; - font-size: 1.3rem; - font-weight: 400; - transition: transform 0.15s ease; -} - -.faqItem[open] .faqQuestion::after { - transform: rotate(45deg); -} - -.faqAnswer { - margin: 0.25rem 0 0; - color: #4b5563; - font-size: 1rem; - line-height: 1.65; -} - .ctaRow { margin-top: 0.25rem; } .cta { display: inline-block; - background: #3b82f6; - color: #ffffff; + background: var(--color-blue-500); + color: var(--color-white); border: none; border-radius: 8px; padding: 0.85rem 2.25rem; font-size: 1rem; - font-weight: 600; + font-weight: 500; text-decoration: none; box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.45); transition: - background 0.15s ease, - transform 0.15s ease; + background 0.15s ease-out, + transform 0.15s ease-out; } .cta:hover { - background: #2563eb; + background: var(--color-blue-600); transform: translateY(-1px); } +.cta:focus-visible { + outline: 2px solid var(--color-blue-500); + outline-offset: 4px; +} + +/* The one deliberate navy accent surface on this page — closing CTA band. */ +.ctaBand { + width: 100vw; + margin-left: calc(50% - 50vw); + background: var(--color-navy-900); + padding: clamp(3rem, 6vw, 4.5rem) 1.5rem; + display: flex; + justify-content: center; +} + +.ctaBandInner { + width: min(560px, 100%); + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; + text-align: center; +} + +.ctaBandHeading { + margin: 0; + color: var(--color-white); + font-size: 1.5rem; + font-weight: 500; +} + @media (max-width: 768px) { .page { - padding: 2.5rem 1.25rem 4rem; + padding: 2.5rem 1.25rem 0; } .article { diff --git a/src/pages/guides/SplitStepAssembly.tsx b/src/pages/guides/SplitStepAssembly.tsx index dc7fce6..de6399a 100644 --- a/src/pages/guides/SplitStepAssembly.tsx +++ b/src/pages/guides/SplitStepAssembly.tsx @@ -1,8 +1,15 @@ import type { ReactNode } from "react"; import { Link } from "react-router-dom"; import { Head } from "vite-react-ssg"; +import AccordionItem from "../../components/AccordionItem"; +import CornerMark from "../../components/CornerMark"; +import Reveal from "../../components/Reveal"; import styles from "./SplitStepAssembly.module.css"; +const MARK_STAGGER_MS = 130; +const cardMarkDelay = (index: number, offset: number) => + index * 90 + offset; + function ExternalLink({ href, children, @@ -166,6 +173,12 @@ export default function SplitStepAssembly() {
+ + ← Back to CAD Viewer @@ -208,10 +221,25 @@ export default function SplitStepAssembly() { Why you'd need to do this
    - {WHY_REASONS.map((reason) => ( -
  • + {WHY_REASONS.map((reason, index) => ( + + + {reason} -
  • + ))}
@@ -237,7 +265,22 @@ export default function SplitStepAssembly() {
    {earlySteps.map((step, index) => ( -
  1. + + + {index + 1}

    {step.title}

    @@ -245,11 +288,17 @@ export default function SplitStepAssembly() { {step.description}

    -
  2. + ))}
+ + CAD Viewer showing a multi-part STEP assembly disassembled into separate components, with one part selected and ready to export as its own STEP file {laterSteps.map((step, index) => ( -
  • + + + {earlySteps.length + index + 1} @@ -272,7 +339,7 @@ export default function SplitStepAssembly() { {step.description}

  • - + ))} @@ -284,26 +351,43 @@ export default function SplitStepAssembly() {

    Frequently asked

    - {FAQS.map((faq) => ( -
    - - {faq.question} - -

    {faq.answer}

    -
    + {FAQS.map((faq, index) => ( + + + + + + ))}
    - -
    -
    - - Try it now — upload your STEP file - -
    -
    + +
    +
    +

    + Ready to pull that part out? +

    +
    + + Try it now — upload your STEP file + +
    +
    +
    ); } diff --git a/src/pages/guides/StepFileTo2dDrawing.module.css b/src/pages/guides/StepFileTo2dDrawing.module.css index ba802fc..1a1c965 100644 --- a/src/pages/guides/StepFileTo2dDrawing.module.css +++ b/src/pages/guides/StepFileTo2dDrawing.module.css @@ -2,6 +2,7 @@ overflow-y: auto; height: auto; min-height: 100vh; + font-family: var(--font-display); } :global(body.guide-page #root) { @@ -11,11 +12,11 @@ .page { min-height: 100vh; - background: #fafafa; - color: #1a1a1a; + background: var(--color-bg); + color: var(--color-ink); display: flex; justify-content: center; - padding: 4rem 1.5rem 6rem; + padding: 4rem 1.5rem 0; } .article { @@ -23,9 +24,11 @@ display: flex; flex-direction: column; gap: clamp(3rem, 5vw, 4rem); + padding-bottom: clamp(3rem, 5vw, 4rem); } .section { + position: relative; display: flex; flex-direction: column; gap: 1.25rem; @@ -34,21 +37,24 @@ .breadcrumb { display: inline-flex; align-self: flex-start; - color: #4b5563; + color: var(--color-ink-muted); font-size: 0.9rem; text-decoration: none; } .breadcrumb:hover { - color: #3b82f6; + color: var(--color-blue-500); +} + +.breadcrumb:focus-visible { + outline: 2px solid var(--color-blue-500); + outline-offset: 2px; } .eyebrow { margin: 0; - color: #6b7280; - font-family: - "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; + color: var(--color-ink-faint); + font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; @@ -56,36 +62,36 @@ .headline { margin: 0.5rem 0 0; - color: #1a1a1a; + color: var(--color-ink); font-size: 2.25rem; line-height: 1.2; - font-weight: 700; + font-weight: 500; } .intro { margin: 0; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1.1rem; line-height: 1.7; } .lede { margin: 0; - color: #1f2937; + color: var(--color-ink); font-size: 1.1rem; line-height: 1.7; } .sectionHeading { margin: 0; - color: #1a1a1a; + color: var(--color-ink); font-size: 1.5rem; - font-weight: 700; + font-weight: 500; } .paragraph { margin: 0; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1.05rem; line-height: 1.7; } @@ -93,7 +99,7 @@ .list { margin: 0; padding-left: 1.25rem; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1.05rem; line-height: 1.7; display: flex; @@ -102,13 +108,18 @@ } .externalLink { - color: #2563eb; + color: var(--color-blue-600); text-decoration: underline; text-underline-offset: 2px; } .externalLink:hover { - color: #1d4ed8; + color: var(--color-blue-700); +} + +.externalLink:focus-visible { + outline: 2px solid var(--color-blue-500); + outline-offset: 2px; } /* Numbered step cards */ @@ -122,13 +133,20 @@ } .stepCard { + position: relative; display: flex; align-items: flex-start; gap: 1.1rem; - background: #ffffff; - border: 1px solid #e5e7eb; + background: var(--color-white); + border: 1px solid var(--color-border); border-radius: 12px; padding: 1.25rem 1.5rem; + transition: border-color 0.14s ease-out; +} + +.stepCard:hover, +.stepCard:focus-within { + border-color: var(--color-blue-500); } .stepNumber { @@ -139,10 +157,11 @@ width: 2.5rem; height: 2.5rem; border-radius: 999px; - background: #eff6ff; - color: #3b82f6; + background: var(--color-blue-50); + color: var(--color-blue-500); + font-family: var(--font-mono); font-size: 1.15rem; - font-weight: 700; + font-weight: 500; } .stepBody { @@ -153,19 +172,20 @@ .stepTitle { margin: 0; - color: #1a1a1a; + color: var(--color-ink); font-size: 1.05rem; - font-weight: 700; + font-weight: 500; } .stepDescription { margin: 0; - color: #4b5563; + color: var(--color-ink-muted); font-size: 1rem; line-height: 1.6; } .stepImageWrap { + position: relative; margin: 0.5rem 0; } @@ -177,7 +197,7 @@ margin: 0.5rem 0; border-radius: 8px; overflow: hidden; - background: #000; + background: var(--color-ink); } .videoWrap iframe { @@ -195,83 +215,65 @@ gap: 0.75rem; } -.faqItem { - background: #ffffff; - border: 1px solid #e5e7eb; - border-radius: 12px; - padding: 0.35rem 1.5rem; -} - -.faqItem[open] { - padding-bottom: 1.25rem; -} - -.faqQuestion { - margin: 0; - padding: 1rem 0; - color: #1a1a1a; - font-size: 1.05rem; - font-weight: 600; - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.faqQuestion::-webkit-details-marker { - display: none; -} - -.faqQuestion::after { - content: "+"; - flex: none; - color: #3b82f6; - font-size: 1.3rem; - font-weight: 400; - transition: transform 0.15s ease; -} - -.faqItem[open] .faqQuestion::after { - transform: rotate(45deg); -} - -.faqAnswer { - margin: 0.25rem 0 0; - color: #4b5563; - font-size: 1rem; - line-height: 1.65; -} - .ctaRow { margin-top: 0.25rem; } .cta { display: inline-block; - background: #3b82f6; - color: #ffffff; + background: var(--color-blue-500); + color: var(--color-white); border: none; border-radius: 8px; padding: 0.85rem 2.25rem; font-size: 1rem; - font-weight: 600; + font-weight: 500; text-decoration: none; box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.45); transition: - background 0.15s ease, - transform 0.15s ease; + background 0.15s ease-out, + transform 0.15s ease-out; } .cta:hover { - background: #2563eb; + background: var(--color-blue-600); transform: translateY(-1px); } +.cta:focus-visible { + outline: 2px solid var(--color-blue-500); + outline-offset: 4px; +} + +/* The one deliberate navy accent surface on this page — closing CTA band. */ +.ctaBand { + width: 100vw; + margin-left: calc(50% - 50vw); + background: var(--color-navy-900); + padding: clamp(3rem, 6vw, 4.5rem) 1.5rem; + display: flex; + justify-content: center; +} + +.ctaBandInner { + width: min(560px, 100%); + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; + text-align: center; +} + +.ctaBandHeading { + margin: 0; + color: var(--color-white); + font-size: 1.5rem; + font-weight: 500; +} + @media (max-width: 768px) { .page { - padding: 2.5rem 1.25rem 4rem; + padding: 2.5rem 1.25rem 0; } .article { diff --git a/src/pages/guides/StepFileTo2dDrawing.tsx b/src/pages/guides/StepFileTo2dDrawing.tsx index fbb018e..ea9bf3c 100644 --- a/src/pages/guides/StepFileTo2dDrawing.tsx +++ b/src/pages/guides/StepFileTo2dDrawing.tsx @@ -1,8 +1,15 @@ import type { ReactNode } from "react"; import { Link } from "react-router-dom"; import { Head } from "vite-react-ssg"; +import AccordionItem from "../../components/AccordionItem"; +import CornerMark from "../../components/CornerMark"; +import Reveal from "../../components/Reveal"; import styles from "./StepFileTo2dDrawing.module.css"; +const MARK_STAGGER_MS = 130; +const cardMarkDelay = (index: number, offset: number) => + index * 90 + offset; + function ExternalLink({ href, children, @@ -176,6 +183,12 @@ export default function StepFileTo2dDrawing() {
    + + ← Back to CAD Viewer @@ -214,7 +227,17 @@ export default function StepFileTo2dDrawing() {

    How to do it

      -
    1. + + + 1

      {STEPS[0].title}

      @@ -222,9 +245,19 @@ export default function StepFileTo2dDrawing() { {STEPS[0].description}

      -
    2. + -
    3. + + + 2

      {STEPS[1].title}

      @@ -232,10 +265,16 @@ export default function StepFileTo2dDrawing() { {STEPS[1].description}

      -
    4. +
    -
    + + + CAD Viewer's 3D viewer showing an uploaded Pump Housing Face Plate STEP file with the Generate 2D Drawing button highlighted in the side panel -
    +
      -
    1. + + + 3

      {STEPS[2].title}

      {STEPS[2].description}
      -
    2. +
    -
    + + + CAD Viewer's 2D drawing editor showing a fully dimensioned Front, Top, and Right orthographic drawing with an isometric reference view, notes, and title block -
    +
      -
    1. + + + 4

      {STEPS[3].title}

      @@ -276,7 +341,7 @@ export default function StepFileTo2dDrawing() { {STEPS[3].description}

      -
    2. +
    @@ -305,26 +370,43 @@ export default function StepFileTo2dDrawing() {

    Frequently asked

    - {FAQS.map((faq) => ( -
    - - {faq.question} - -

    {faq.answer}

    -
    + {FAQS.map((faq, index) => ( + + + + + + ))}
    - -
    - -
    + +
    +
    +

    + Get a real drawing out of your model +

    +
    + + Try it now — upload your STEP file + +
    +
    +
    ); } diff --git a/src/pages/illustrations.tsx b/src/pages/illustrations.tsx new file mode 100644 index 0000000..a969afb --- /dev/null +++ b/src/pages/illustrations.tsx @@ -0,0 +1,108 @@ +// Simple thin-line mechanical illustrations for the About page — bolt, +// calipers, bracket. Deliberately schematic rather than photorealistic; +// stroke-only in the brand blue to match TriangleMark's visual language. + +type IllustrationProps = { + size?: number; + className?: string; +}; + +const SHARED_PROPS = { + fill: "none", + stroke: "var(--color-blue-500)", + strokeWidth: 2, + strokeLinecap: "round" as const, + strokeLinejoin: "round" as const, + "aria-hidden": true, + focusable: false, +}; + +export function BoltIllustration({ size = 96, className }: IllustrationProps) { + return ( + + {/* Hex head */} + + + {/* Shaft */} + + + + + ); +} + +export function CalipersIllustration({ + size = 96, + className, +}: IllustrationProps) { + return ( + + {/* Beam (scale bar) */} + + {/* Scale ticks */} + + {/* Fixed jaw, foot pointing right */} + + {/* Sliding jaw, foot pointing left to face the fixed jaw */} + + {/* Thumbwheel on the sliding jaw */} + + + + ); +} + +// Wireframe box — the viewer's true-empty-state placeholder (no file +// loaded yet). Removed the instant a file loads; not used anywhere else. +export function EmptyViewportIllustration({ + size = 96, + className, +}: IllustrationProps) { + return ( + + + + + ); +} + +export function BracketIllustration({ + size = 96, + className, +}: IllustrationProps) { + return ( + + {/* Top (L-shaped) face */} + + {/* Thickness / side faces at the outer corner */} + + {/* Mounting holes */} + + + + ); +} diff --git a/src/pages/landingIcons.tsx b/src/pages/landingIcons.tsx new file mode 100644 index 0000000..8ece18e --- /dev/null +++ b/src/pages/landingIcons.tsx @@ -0,0 +1,98 @@ +// Icon tiles for the Landing page hero/capability section — a rounded +// swatch with four corner-bracket accents (a static echo of CornerMark's +// registration-mark motif, used here since these tiles animate in as a +// unit via Reveal rather than drawing in on their own). +import type { ReactNode } from "react"; +import styles from "./Landing.module.css"; + +export function IconTile({ + size = "sm", + children, +}: { + size?: "sm" | "lg"; + children: ReactNode; +}) { + return ( +
    + + + + + {children} +
    + ); +} + +const STROKE = { + fill: "none", + stroke: "var(--color-blue-500)", + "aria-hidden": true, + focusable: false, +} as const; + +export function ViewFormatsIcon({ size = 34 }: { size?: number }) { + return ( + + + + + ); +} + +export function PdfExportIcon({ size = 28 }: { size?: number }) { + return ( + + + + + + + + + ); +} + +export function AssemblyIcon({ size = 28 }: { size?: number }) { + return ( + + + + + + + + ); +} + +export function MeasureIcon({ size = 28 }: { size?: number }) { + return ( + + + + + + + ); +} + +export function UploadIcon({ size = 26 }: { size?: number }) { + return ( + + + + + + ); +} + +export function ConnectorArrowIcon({ size = 18 }: { size?: number }) { + return ( + + + + ); +} diff --git a/src/styles/design-tokens.css b/src/styles/design-tokens.css new file mode 100644 index 0000000..26a0b72 --- /dev/null +++ b/src/styles/design-tokens.css @@ -0,0 +1,78 @@ +/* Design tokens — single source of truth for color and type across the + * site. Every color/font reference sitewide should resolve through these + * custom properties rather than a hardcoded literal. Mirrors the + * "one canonical file" pattern used by drafting-rules.ts for the drawing + * sheet — this is the equivalent file for the brand/UI layer, and is a + * completely separate system from that one (see drafting-rules.ts). + */ + +@font-face { + font-family: "IBM Plex Sans"; + src: url("/fonts/ibm-plex-sans-400.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Sans"; + src: url("/fonts/ibm-plex-sans-500.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/ibm-plex-mono-400.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/ibm-plex-mono-500.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +:root { + /* Brand blue ramp */ + --color-blue-50: #eff6ff; + --color-blue-100: #dbeafe; + --color-blue-500: #3b82f6; + --color-blue-600: #2563eb; + --color-blue-700: #1d4ed8; + + /* The one "blueprint" accent surface — used sparingly, one per page */ + --color-navy-900: #0f2557; + + /* Secondary accent — export/download actions only, at most one per page */ + --color-amber-50: #fef3c7; + --color-amber-500: #f59e0b; + --color-amber-700: #b45309; + + /* Ink / neutral ramp — consolidates the three near-duplicate grayscales + * that previously existed independently in Landing.module.css, the + * guide/About pages, and src/ui/App.css. */ + --color-ink: #0f172a; + --color-ink-muted: #475569; + --color-ink-faint: #64748b; + --color-border: #d9dee5; + --color-bg: #f7f8fa; + --color-bg-alt: #eef1f5; + --color-white: #ffffff; + + /* Destructive/error — not part of the brand ramp, kept separate on + * purpose so error states never get confused with brand blue/navy/amber. */ + --color-error-500: #dc2626; + --color-error-50: #fef2f2; + + /* Type */ + --font-display: "IBM Plex Sans", "Segoe UI", -apple-system, BlinkMacSystemFont, + "Helvetica Neue", Arial, sans-serif; + --font-mono: "IBM Plex Mono", "SFMono-Regular", Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; +} diff --git a/src/styles/tokenValues.ts b/src/styles/tokenValues.ts new file mode 100644 index 0000000..4f23259 --- /dev/null +++ b/src/styles/tokenValues.ts @@ -0,0 +1,8 @@ +// Plain JS values mirroring design-tokens.css, for the handful of +// consumers (Three.js renderer clear color, canvas fillStyle, etc.) that +// can't resolve a CSS custom property. Keep in sync with design-tokens.css. + +// Matches --color-bg. Used as the 3D viewport clear color in both the +// SSG-only ViewerPage and the lazy-loaded App shell, which previously +// carried two slightly different near-white literals for the same job. +export const VIEWER_BG_COLOR = "#f7f8fa"; diff --git a/src/ui/App.css b/src/ui/App.css index 5e7da46..ea4600b 100644 --- a/src/ui/App.css +++ b/src/ui/App.css @@ -1,21 +1,14 @@ :root { - font-family: - "Inter", - "Segoe UI", - -apple-system, - BlinkMacSystemFont, - "Helvetica Neue", - Arial, - sans-serif; - --bg-base: #e2e8f0; - --bg-soft: #f8fafc; + font-family: var(--font-display); + --bg-base: var(--color-bg-alt); + --bg-soft: var(--color-bg); --bg-panel: rgba(255, 255, 255, 0.92); --surface-border: rgba(148, 163, 184, 0.35); - --text-primary: #0f172a; - --text-secondary: #475569; - --text-muted: #64748b; - --brand: #1d4ed8; - --brand-strong: #1e40af; + --text-primary: var(--color-ink); + --text-secondary: var(--color-ink-muted); + --text-muted: var(--color-ink-faint); + --brand: var(--color-blue-700); + --brand-strong: var(--color-blue-700); --brand-soft: rgba(59, 130, 246, 0.14); --shadow-soft: 0 18px 40px rgba(15, 23, 42, 0.08); } @@ -403,8 +396,8 @@ a { height: 100%; display: grid; grid-template-rows: auto auto 1fr; - background: #f7f8fa; - color: #0f172a; + background: var(--color-bg); + color: var(--color-ink); } .cad-app__header { @@ -428,7 +421,7 @@ a { .cad-app__title-wrap h1 { margin: 0; font-size: 20px; - font-weight: 700; + font-weight: 500; } .cad-app__actions { @@ -445,15 +438,15 @@ a { border-radius: 8px; padding: 9px 14px; font-size: 13px; - font-weight: 600; - color: #ffffff; + font-weight: 500; + color: var(--color-white); border: none; - background: #2563eb; + background: var(--color-blue-600); cursor: pointer; } .cad-app__upload-btn:hover { - background: #1d4ed8; + background: var(--color-blue-700); } .cad-app__upload-btn input { @@ -467,9 +460,9 @@ a { border-radius: 8px; border: none; background: transparent; - color: #64748b; + color: var(--color-ink-faint); font-size: 13px; - font-weight: 600; + font-weight: 500; padding: 9px 14px; cursor: pointer; } @@ -477,7 +470,7 @@ a { .cad-app__actions button:hover:not(:disabled) { background: rgba(100, 116, 139, 0.08); border-radius: 8px; - color: #334155; + color: var(--color-ink-muted); } .cad-app__actions button:disabled { @@ -493,12 +486,12 @@ a { padding: 10px 16px; border-bottom: 1px solid rgba(148, 163, 184, 0.25); font-size: 13px; - color: #334155; + color: var(--color-ink-muted); } .cad-app__error { - color: #b91c1c; - font-weight: 600; + color: var(--color-error-500); + font-weight: 500; } .cad-app__viewer { @@ -511,6 +504,25 @@ a { height: 100%; } +.cad-app__viewer-empty { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding-bottom: 12%; + gap: 0.75rem; + pointer-events: none; +} + +.cad-app__viewer-empty p { + margin: 0; + color: var(--color-ink-faint); + font-family: var(--font-display); + font-size: 0.9rem; +} + @media (max-width: 1080px) { .feature-grid, .audience-grid { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 588961a..62efc26 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,7 +2,9 @@ import { useCallback, useEffect, useMemo, useState, startTransition } from "reac import { Link } from "react-router-dom"; import { consumePendingFile } from "../fileStore"; import { CAD_EXTS, CadViewer, MESH_ASSEMBLY_EXTS } from "../components/cad/cad-viewer"; +import { EmptyViewportIllustration } from "../pages/illustrations"; import TriangleMark from "../pages/TriangleMark"; +import { VIEWER_BG_COLOR } from "../styles/tokenValues"; import "./App.css"; const ACCEPTED_FORMATS = [ @@ -102,7 +104,7 @@ export default function App() { onDrop={handleViewerDrop} style={{ colorScheme: "light", - outline: viewerDragging ? "2px solid #3b82f6" : "none", + outline: viewerDragging ? "2px solid var(--color-blue-500)" : "none", outlineOffset: "-2px", transition: "outline 0.12s ease", }} @@ -141,9 +143,15 @@ export default function App() { showControls showViewCube showHomeButton - backgroundColor="#f0f2f5" + backgroundColor={VIEWER_BG_COLOR} className="cad-viewer-host" /> + {!file && ( + + )} {viewerDragging && ( @@ -167,10 +175,10 @@ export default function App() { border: "1px solid rgba(59,130,246,0.4)", borderRadius: "10px", padding: "0.7rem 1.6rem", - color: "#3b82f6", + color: "var(--color-blue-500)", fontSize: "1rem", - fontWeight: 600, - fontFamily: "monospace", + fontWeight: 500, + fontFamily: "var(--font-mono)", letterSpacing: "0.04em", }} > diff --git a/src/ui/LoadingOverlay.tsx b/src/ui/LoadingOverlay.tsx index 0519b57..ec59372 100644 --- a/src/ui/LoadingOverlay.tsx +++ b/src/ui/LoadingOverlay.tsx @@ -51,7 +51,7 @@ export default function LoadingOverlay({ >
    @@ -82,8 +83,8 @@ export default function LoadingOverlay({
    {fmtSize(fileSize)}
    -
    +
    {stage} {dots}
    @@ -112,13 +123,15 @@ export default function LoadingOverlay({ marginBottom: "0.4rem", }} > - Loading + + Loading + {Math.round(progress)}% @@ -128,7 +141,7 @@ export default function LoadingOverlay({ style={{ width: "100%", height: "6px", - background: "#e5e7eb", + background: "var(--color-border)", borderRadius: "999px", overflow: "hidden", }} @@ -137,7 +150,8 @@ export default function LoadingOverlay({ style={{ height: "100%", width: `${progress}%`, - background: "linear-gradient(90deg, #3b82f6, #60a5fa)", + background: + "linear-gradient(90deg, var(--color-blue-500), #60a5fa)", borderRadius: "999px", transition: "width 0.35s ease", }} @@ -148,7 +162,7 @@ export default function LoadingOverlay({
    diff --git a/src/ui/MarketingLayout.tsx b/src/ui/MarketingLayout.tsx new file mode 100644 index 0000000..ae516e0 --- /dev/null +++ b/src/ui/MarketingLayout.tsx @@ -0,0 +1,25 @@ +// Shared layout for the marketing/content routes (Home, About, Guides) — +// a subtle fade between them on navigation. Deliberately not used for the +// /viewer route, which is a sibling top-level route outside this layout +// and stays instant. +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; +import { Outlet, useLocation } from "react-router-dom"; + +export default function MarketingLayout() { + const location = useLocation(); + const prefersReducedMotion = useReducedMotion(); + + return ( + + + + + + ); +} diff --git a/src/ui/ViewerPage.tsx b/src/ui/ViewerPage.tsx index 943d00b..0d90b25 100644 --- a/src/ui/ViewerPage.tsx +++ b/src/ui/ViewerPage.tsx @@ -1,7 +1,9 @@ import React, { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { CAD_EXTS, CadViewer, MESH_ASSEMBLY_EXTS } from "../components/cad/cad-viewer"; +import { EmptyViewportIllustration } from "../pages/illustrations"; import TriangleMark from "../pages/TriangleMark"; +import { VIEWER_BG_COLOR } from "../styles/tokenValues"; const ACCEPTED_FORMATS = [ ".step", @@ -94,9 +96,15 @@ export default function ViewerPage() { showControls showViewCube showHomeButton - backgroundColor="#f1f5f9" + backgroundColor={VIEWER_BG_COLOR} className="cad-viewer-host" /> + {!file && ( + + )}
    );