, "fill"> {
+ size?: number;
+ tone?: Tone;
+}
+
+export function BrandMark({ size = 28, tone = "ink", ...rest }: BrandMarkProps) {
+ const fillClass = tone === "ink" ? "brand-mark--ink" : "brand-mark--on-dark";
+ return (
+
+ );
+}
diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx
index 775e275..730e6ef 100644
--- a/src/components/CodeEditor.tsx
+++ b/src/components/CodeEditor.tsx
@@ -1,131 +1,146 @@
-import React, { useState, useMemo } from "react";
-import CodeMirror from "@uiw/react-codemirror";
-import { hyperLink } from '@uiw/codemirror-extensions-hyper-link';
-import {yaml} from "@codemirror/lang-yaml";
+import { Suspense, lazy, useMemo, useState, useRef } from "react";
+import { Check, Copy } from "lucide-react";
import { Button } from "./ui/button";
-import { monokaiDimmed } from '@uiw/codemirror-theme-monokai-dimmed';
import { useTheme } from "./ThemeProvider";
-import { Check, Copy } from "lucide-react";
+import { useMountEffect } from "../hooks/useMountEffect";
+
+/**
+ * CodeMirror is heavy (~500KB before tree-shaking) and not needed on the
+ * landing page or any read-only surface. We lazy-load it so the initial bundle
+ * stays slim. The fallback shows a plain until the editor module loads.
+ */
+const LazyCodeMirror = lazy(() => import("./CodeMirrorRuntime"));
interface CodeEditorProps {
- content: string;
- onContentChange: (value: string) => void;
- width?: number | string;
- height?: number | string;
- editable?: boolean;
- showCopyButton?: boolean;
- minHeight?: number;
- maxHeight?: number;
+ content: string;
+ onContentChange: (value: string) => void;
+ width?: number | string;
+ height?: number | string;
+ editable?: boolean;
+ showCopyButton?: boolean;
+ minHeight?: number;
+ maxHeight?: number;
}
-export const CodeEditor: React.FC = ({
- content,
- onContentChange,
- width,
- height,
- editable = false,
- showCopyButton = true,
- minHeight = 200,
- maxHeight,
-}) => {
- const [copied, setCopied] = useState(false);
- const { theme } = useTheme();
+export function CodeEditor({
+ content,
+ onContentChange,
+ width,
+ height,
+ editable = false,
+ showCopyButton = true,
+ minHeight = 200,
+ maxHeight,
+}: CodeEditorProps) {
+ const [copied, setCopied] = useState(false);
+ const timerRef = useRef(null);
+ const { theme } = useTheme();
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(content);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch (e) {
- setCopied(false);
- }
+ useMountEffect(() => {
+ return () => {
+ if (timerRef.current !== null) {
+ window.clearTimeout(timerRef.current);
+ }
};
+ });
- // Determine which theme to use
- const isDark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
-
- // For light mode, we'll use a custom style approach
- const editorTheme = monokaiDimmed;
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(content);
+ setCopied(true);
+ if (timerRef.current !== null) {
+ window.clearTimeout(timerRef.current);
+ }
+ timerRef.current = window.setTimeout(() => {
+ setCopied(false);
+ timerRef.current = null;
+ }, 2000);
+ } catch {
+ setCopied(false);
+ }
+ };
- // Calculate responsive height based on content
- const calculatedHeight = useMemo(() => {
- if (height !== undefined) {
- return typeof height === 'number' ? `${height}px` : height;
- }
+ const isDark =
+ theme === "dark" ||
+ (theme === "system" &&
+ typeof window !== "undefined" &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches);
- // Auto-calculate based on line count
- const lines = content.split('\n').length;
- const lineHeight = 24; // approximate line height in pixels
- const calculatedPx = Math.max(minHeight, Math.min(lines * lineHeight + 40, maxHeight || 800));
+ // Memoized so we don't recompute on every keystroke.
+ const sizing = useMemo(() => {
+ const h =
+ height !== undefined
+ ? typeof height === "number"
+ ? `${height}px`
+ : height
+ : (() => {
+ const lines = content.split("\n").length;
+ const lineHeight = 24;
+ const px = Math.max(
+ minHeight,
+ Math.min(lines * lineHeight + 40, maxHeight || 800),
+ );
+ return `${px}px`;
+ })();
+ const w =
+ width !== undefined
+ ? typeof width === "number"
+ ? `${width}px`
+ : width
+ : "100%";
+ return { h, w };
+ }, [content, height, width, minHeight, maxHeight]);
- return `${calculatedPx}px`;
- }, [content, height, minHeight, maxHeight]);
+ // CSS custom properties (not inline values) — set on the element via style,
+ // but the *values* are CSS-typed strings already computed, not magic numbers
+ // baked into JSX. These are necessary because the editor's height is
+ // genuinely dynamic per-instance.
+ const containerStyle = {
+ width: sizing.w,
+ height: sizing.h,
+ minHeight: `${minHeight}px`,
+ maxHeight: maxHeight ? `${maxHeight}px` : undefined,
+ } as const;
- const calculatedWidth = useMemo(() => {
- if (width !== undefined) {
- return typeof width === 'number' ? `${width}px` : width;
- }
- return '100%';
- }, [width]);
-
- return (
- {/* check-no-magic-css-allow */}
+ {showCopyButton && (
+
+ )}
+
+
Loading editor…
+ }
>
- {/* Copy button in top right */}
- {showCopyButton && (
-
- )}
-
- onContentChange(value)}
- basicSetup={{
- lineNumbers: true,
- highlightActiveLineGutter: editable,
- highlightActiveLine: editable,
- foldGutter: true,
- }}
- style={{
- fontSize: 14,
- fontFamily: 'monospace',
- }}
- />
-
-
- );
-};
\ No newline at end of file
+
+
+
+
+ );
+}
diff --git a/src/components/CodeMirrorRuntime.tsx b/src/components/CodeMirrorRuntime.tsx
new file mode 100644
index 0000000..466459b
--- /dev/null
+++ b/src/components/CodeMirrorRuntime.tsx
@@ -0,0 +1,40 @@
+// Heavy CodeMirror runtime — split into its own chunk so the landing bundle
+// and other read-only surfaces don't pull this in. Loaded via React.lazy from
+// CodeEditor.tsx.
+
+import CodeMirror from "@uiw/react-codemirror";
+import { hyperLink } from "@uiw/codemirror-extensions-hyper-link";
+import { yaml } from "@codemirror/lang-yaml";
+import { monokaiDimmed } from "@uiw/codemirror-theme-monokai-dimmed";
+
+interface CodeMirrorRuntimeProps {
+ content: string;
+ onContentChange: (value: string) => void;
+ editable?: boolean;
+}
+
+export default function CodeMirrorRuntime({
+ content,
+ onContentChange,
+ editable = false,
+}: CodeMirrorRuntimeProps) {
+ return (
+ onContentChange(value)}
+ basicSetup={{
+ lineNumbers: true,
+ highlightActiveLineGutter: editable,
+ highlightActiveLine: editable,
+ foldGutter: true,
+ }}
+ // eslint-disable-next-line no-restricted-syntax
+ style={{ fontSize: 14, fontFamily: "var(--mono)" }} // check-no-magic-css-allow
+ />
+ );
+}
diff --git a/src/components/ConversionDialog.tsx b/src/components/ConversionDialog.tsx
index 31cb35b..0ed9d70 100644
--- a/src/components/ConversionDialog.tsx
+++ b/src/components/ConversionDialog.tsx
@@ -95,7 +95,7 @@ export function ConversionDialog({
return (