diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..81ada07 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: + +permissions: + contents: read + +jobs: + frontend: + name: Frontend checks + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + backend: + name: Backend checks + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: backend/requirements.txt + + - name: Install dependencies + run: python -m pip install -r requirements.txt + + - name: Compile backend + run: python -m compileall -q app diff --git a/backend/app/ai/context.py b/backend/app/ai/context.py index 86041bc..78ea93e 100644 --- a/backend/app/ai/context.py +++ b/backend/app/ai/context.py @@ -11,12 +11,12 @@ Keep answers useful and concise, with short headings or bullets when appropriate. Format every answer for a human reader: 1. Start with a direct answer in one or two sentences. -2. Use short Markdown headings beginning with ### when sections are useful. +2. Use short Markdown headings beginning with -> when sections are useful. 3. Use simple bullet points for evidence and file paths in backticks. 4. End with a brief limitation statement only when the supplied context is incomplete. Do not return JSON, XML, provider metadata, or internal reasoning.""" -MAX_CONTEXT_CHARS = 18000 +MAX_CONTEXT_CHARS = 12000 def _compact_analysis(analysis: dict): @@ -35,8 +35,13 @@ def _compact_analysis(analysis: dict): def repository_context(analysis: dict, chunks: list[dict]): + compact = _compact_analysis(analysis) + architecture = compact.get("architecture", {}) + architecture["module_graph"] = architecture.get("module_graph", [])[:20] + compact["source_analysis"] = compact.get("source_analysis", [])[:25] + compact["architecture"] = architecture return { - "analysis": _compact_analysis(analysis), + "analysis": compact, "retrieved_code_chunks": [ { "file": chunk["file_path"], @@ -44,7 +49,7 @@ def repository_context(analysis: dict, chunks: list[dict]): "chunk_index": chunk["chunk_index"], "content": chunk["content"][:2400] } - for chunk in chunks[:4] + for chunk in chunks[:2] ] } @@ -77,7 +82,17 @@ def profile_context(repositories: list[dict]): def build_messages(question: str, context: dict): context_text = json.dumps(context, separators=(",", ":"), default=str) if len(context_text) > MAX_CONTEXT_CHARS: - context_text = context_text[:MAX_CONTEXT_CHARS] + "\n[context truncated]" + context_text = json.dumps( + { + **context, + "retrieved_code_chunks": context.get("retrieved_code_chunks", [])[:1], + "context_note": "Some low-priority context was omitted to preserve response speed." + }, + separators=(",", ":"), + default=str + ) + if len(context_text) > MAX_CONTEXT_CHARS: + context_text = context_text[:MAX_CONTEXT_CHARS] + "\n[context truncated]" return [ {"role": "system", "content": SYSTEM_PROMPT}, diff --git a/backend/app/ai/llm.py b/backend/app/ai/llm.py index de551d0..85a6b89 100644 --- a/backend/app/ai/llm.py +++ b/backend/app/ai/llm.py @@ -39,11 +39,12 @@ async def _request_provider(messages: list[dict], provider_name: str): if not settings["key"] or settings["key"].startswith("replace-after"): raise AIProviderError(f"{provider_name} API key is not configured") + configured_model = AI_MODEL if provider_name == AI_PROVIDER else None payload = { - "model": AI_MODEL or settings["default_model"], + "model": configured_model or settings["default_model"], "messages": messages, "temperature": 0.2, - "max_tokens": 1200 + "max_tokens": 900 } headers = { "Authorization": f"Bearer {settings['key']}", @@ -74,6 +75,11 @@ async def ask_llm(messages: list[dict], provider: str | None = None): return await _request_provider(messages, provider_name) except AIProviderError as error: error_text = str(error).lower() - if provider is None and provider_name == "groq" and "context_length" in error_text: + if ( + provider is None + and provider_name == "groq" + and ("context_length" in error_text or "rate_limit" in error_text) + and MISTRAL_API_KEY + ): return await _request_provider(messages, "mistral") raise \ No newline at end of file diff --git a/backend/app/ai/retrieval.py b/backend/app/ai/retrieval.py index 1b1e414..7ed1b36 100644 --- a/backend/app/ai/retrieval.py +++ b/backend/app/ai/retrieval.py @@ -38,10 +38,12 @@ async def index_repository(access_token: str, owner: str, repo: str): async def retrieve_chunks(access_token: str, owner: str, repo: str, question: str, limit: int = 3): full_name = f"{owner}/{repo}" - chunks = get_code_chunks(full_name) + repository = await get_repository(access_token, owner, repo) + source_version = repository.get("pushed_at") or repository.get("updated_at") + chunks = get_code_chunks(full_name, source_version) if not chunks: await index_repository(access_token, owner, repo) - chunks = get_code_chunks(full_name) + chunks = get_code_chunks(full_name, source_version) query_embedding = await embed_texts([question]) question_terms = _terms(question) @@ -49,7 +51,10 @@ async def retrieve_chunks(access_token: str, owner: str, repo: str, question: st for chunk in chunks: score = 0.0 if query_embedding and chunk.get("embedding"): - score = cosine_similarity(query_embedding[0], json.loads(chunk["embedding"])) + try: + score = cosine_similarity(query_embedding[0], json.loads(chunk["embedding"])) + except (TypeError, ValueError, json.JSONDecodeError): + score = 0.0 else: content_terms = _terms(chunk["file_path"] + " " + chunk["content"]) score = len(question_terms & content_terms) / max(len(question_terms), 1) diff --git a/backend/app/analyzers/architecture_analyzer.py b/backend/app/analyzers/architecture_analyzer.py index e8335c2..a0cfd2e 100644 --- a/backend/app/analyzers/architecture_analyzer.py +++ b/backend/app/analyzers/architecture_analyzer.py @@ -1,4 +1,44 @@ -def analyze_architecture(files: list[str], technologies: list[str]): +from pathlib import PurePosixPath + + +def _module_candidates(import_name: str, source_file: str): + if not import_name.startswith("."): + return [] + + base = PurePosixPath(source_file).parent + relative = import_name.lstrip(".").replace(".", "/") + if import_name.startswith(".."): + base = base.parent + target = str(base / relative).replace("\\", "/").lstrip("./") + return [ + target, + *[f"{target}.{extension}" for extension in ("py", "js", "jsx", "ts", "tsx")], + f"{target}/index.js", + f"{target}/index.ts", + f"{target}/__init__.py" + ] + + +def _build_module_graph(source_analysis: list[dict]): + known_files = {item["file"] for item in source_analysis} + graph = [] + for item in source_analysis: + for import_name in item.get("imports", []): + target = next( + (candidate for candidate in _module_candidates(import_name, item["file"]) + if candidate in known_files), + None + ) + if target: + graph.append({"from": item["file"], "to": target, "import": import_name}) + return graph[:100] + + +def analyze_architecture( + files: list[str], + technologies: list[str], + source_analysis: list[dict] | None = None +): lower_files = [path.lower() for path in files] lower_technologies = {technology.lower() for technology in technologies} layers = [] @@ -28,10 +68,27 @@ def analyze_architecture(files: list[str], technologies: list[str]): else: architecture_type = "Unclassified" + source_analysis = source_analysis or [] + module_graph = _build_module_graph(source_analysis) + entry_points = [ + path for path in files + if PurePosixPath(path).name.lower() in { + "main.py", "app.py", "server.py", "index.js", "main.jsx", "main.tsx" + } + ] + return { "architecture_type": architecture_type, "frontend": next((name for name in ("React", "Vue", "Angular", "Next.js") if name.lower() in lower_technologies), None), "backend": next((name for name in ("FastAPI", "Flask", "Django", "Node.js") if name.lower() in lower_technologies), None), "external_services": ["GitHub API"] if any("github" in path for path in lower_files) else [], - "layers": layers + "layers": layers, + "entry_points": entry_points, + "module_graph": module_graph, + "module_relationships": len(module_graph), + "analyzed_source_files": len(source_analysis), + "analysis_note": ( + "Architecture is inferred from repository paths, detected technologies, " + "and imports in the analyzed source files." + ) } \ No newline at end of file diff --git a/backend/app/analyzers/file_detector.py b/backend/app/analyzers/file_detector.py index cfe4a43..90c6ae3 100644 --- a/backend/app/analyzers/file_detector.py +++ b/backend/app/analyzers/file_detector.py @@ -13,10 +13,14 @@ "docker-compose.yml", "docker-compose.yaml", ".env.example", "README.md" } +SOURCE_EXTENSIONS = {"py", "js", "jsx", "ts", "tsx"} +MAX_ANALYSIS_FILES = 40 + def detect_important_files(files: list[str]): entry_points = [] config_files = [] + source_files = [] for path in files: name = PurePosixPath(path).name @@ -24,10 +28,14 @@ def detect_important_files(files: list[str]): entry_points.append(path) if name in CONFIG_FILE_NAMES or name.startswith("config."): config_files.append(path) + if PurePosixPath(path).suffix.lower().lstrip(".") in SOURCE_EXTENSIONS: + source_files.append(path) - important_files = list(dict.fromkeys(entry_points + config_files)) + analysis_files = list(dict.fromkeys(entry_points + config_files + source_files)) return { "entry_points": entry_points, "config_files": config_files, - "important_files": important_files + "source_files": source_files, + "important_files": analysis_files[:MAX_ANALYSIS_FILES], + "total_candidates": len(analysis_files) } \ No newline at end of file diff --git a/backend/app/database/repositories.py b/backend/app/database/repositories.py index 3516c96..fc41d08 100644 --- a/backend/app/database/repositories.py +++ b/backend/app/database/repositories.py @@ -27,12 +27,18 @@ def replace_code_chunks(repository: str, source_version: str | None, chunks: lis connection.close() -def get_code_chunks(repository: str): +def get_code_chunks(repository: str, source_version: str | None = None): connection = get_connection() - rows = connection.execute( - "SELECT * FROM code_chunks WHERE repository = ?", - (repository,) - ).fetchall() + if source_version is None: + rows = connection.execute( + "SELECT * FROM code_chunks WHERE repository = ?", + (repository,) + ).fetchall() + else: + rows = connection.execute( + "SELECT * FROM code_chunks WHERE repository = ? AND source_version = ?", + (repository, source_version) + ).fetchall() connection.close() return [dict(row) for row in rows] diff --git a/backend/app/services/repo_analyzer.py b/backend/app/services/repo_analyzer.py index 6ae0dec..eecfbda 100644 --- a/backend/app/services/repo_analyzer.py +++ b/backend/app/services/repo_analyzer.py @@ -1,3 +1,5 @@ +import asyncio + from app.services.github_api import ( get_repository, get_repository_languages, @@ -85,15 +87,16 @@ async def analyze_repository( selected_files = important_file_analysis["important_files"] source_contents = {} - for path in selected_files: + async def fetch_source(path: str): try: - content = await get_repository_file( - access_token, - owner, - repo, - path - ) + return path, await get_repository_file(access_token, owner, repo, path) except Exception: + return path, None + + fetched_files = await asyncio.gather(*(fetch_source(path) for path in selected_files)) + + for path, content in fetched_files: + if content is None: continue if path.endswith("package.json"): @@ -142,7 +145,7 @@ async def analyze_repository( "readme": analyze_readme(readme_content), "important_files": important_file_analysis, "source_analysis": source_analysis, - "architecture": analyze_architecture(files, technologies) + "architecture": analyze_architecture(files, technologies, source_analysis) } diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md new file mode 100644 index 0000000..f457973 --- /dev/null +++ b/frontend/DESIGN.md @@ -0,0 +1,537 @@ +--- +version: alpha +name: Cursor-design-analysis +description: An AI-first code editor whose marketing site reads like a quietly-confident developer-tools brand with a warm-cream editorial canvas (`#f7f7f4`) instead of the typical dark IDE atmosphere. Near-black warm ink (`#26251e`) carries body and display alike — display sits at weight 400 with negative letter-spacing for a magazine feel rather than a bold tech voice. The single brand voltage is **Cursor Orange** (`#f54e00`) reserved for primary CTAs and the wordmark. A signature pastel timeline palette (peach, mint, blue, lavender, gold) marks AI-action stages (Thinking / Reading / Editing / Grepping / Done) — only inside in-product timeline visualizations. Cards use minimal hairlines, no shadows, generous 80px section rhythm. CursorGothic for display/body, JetBrains Mono on every code surface (which is roughly half the page). + +colors: + primary: "#f54e00" + primary-active: "#d04200" + ink: "#26251e" + body: "#5a5852" + body-strong: "#26251e" + muted: "#807d72" + muted-soft: "#a09c92" + hairline: "#e6e5e0" + hairline-soft: "#efeee8" + hairline-strong: "#cfcdc4" + canvas: "#f7f7f4" + canvas-soft: "#fafaf7" + surface-card: "#ffffff" + surface-strong: "#e6e5e0" + on-primary: "#ffffff" + timeline-thinking: "#dfa88f" + timeline-grep: "#9fc9a2" + timeline-read: "#9fbbe0" + timeline-edit: "#c0a8dd" + timeline-done: "#c08532" + semantic-error: "#cf2d56" + semantic-success: "#1f8a65" + +typography: + display-mega: + fontFamily: "'CursorGothic', system-ui, 'Helvetica Neue', Helvetica, Arial, sans-serif" + fontSize: 72px + fontWeight: 400 + lineHeight: 1.1 + letterSpacing: -2.16px + display-lg: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 36px + fontWeight: 400 + lineHeight: 1.2 + letterSpacing: -0.72px + display-md: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 26px + fontWeight: 400 + lineHeight: 1.25 + letterSpacing: -0.325px + display-sm: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 22px + fontWeight: 400 + lineHeight: 1.3 + letterSpacing: -0.11px + title-md: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 18px + fontWeight: 600 + lineHeight: 1.4 + letterSpacing: 0 + title-sm: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 16px + fontWeight: 600 + lineHeight: 1.4 + letterSpacing: 0 + body-md: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 16px + fontWeight: 400 + lineHeight: 1.5 + letterSpacing: 0 + body-tracked: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 16px + fontWeight: 400 + lineHeight: 1.5 + letterSpacing: 0.08px + body-sm: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 14px + fontWeight: 400 + lineHeight: 1.5 + letterSpacing: 0 + caption: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 13px + fontWeight: 400 + lineHeight: 1.4 + letterSpacing: 0 + caption-uppercase: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 11px + fontWeight: 600 + lineHeight: 1.4 + letterSpacing: 0.88px + textTransform: uppercase + code: + fontFamily: "'JetBrains Mono', 'Fira Code', monospace" + fontSize: 13px + fontWeight: 400 + lineHeight: 1.5 + letterSpacing: 0 + button: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 14px + fontWeight: 500 + lineHeight: 1.0 + letterSpacing: 0 + nav-link: + fontFamily: "'CursorGothic', sans-serif" + fontSize: 14px + fontWeight: 500 + lineHeight: 1.4 + letterSpacing: 0 + +rounded: + none: 0px + xs: 4px + sm: 6px + md: 8px + lg: 12px + xl: 16px + pill: 9999px + full: 9999px + +spacing: + xxs: 4px + xs: 8px + sm: 12px + base: 16px + md: 20px + lg: 24px + xl: 32px + xxl: 48px + section: 80px + +components: + top-nav: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + typography: "{typography.nav-link}" + height: 64px + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.on-primary}" + typography: "{typography.button}" + rounded: "{rounded.md}" + padding: 10px 18px + height: 40px + button-primary-active: + backgroundColor: "{colors.primary-active}" + textColor: "{colors.on-primary}" + rounded: "{rounded.md}" + button-secondary: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + typography: "{typography.button}" + rounded: "{rounded.md}" + padding: 9px 17px + height: 40px + button-tertiary-text: + backgroundColor: transparent + textColor: "{colors.ink}" + typography: "{typography.button}" + button-download: + backgroundColor: "{colors.ink}" + textColor: "{colors.canvas}" + typography: "{typography.button}" + rounded: "{rounded.md}" + padding: 12px 20px + height: 44px + hero-band: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + typography: "{typography.display-mega}" + padding: 80px + ide-mockup-card: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + rounded: "{rounded.lg}" + padding: 0 + ide-pane: + backgroundColor: "{colors.canvas-soft}" + textColor: "{colors.body}" + typography: "{typography.code}" + rounded: "{rounded.md}" + padding: 16px + feature-card: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + typography: "{typography.title-md}" + rounded: "{rounded.lg}" + padding: 24px + comparison-card: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + typography: "{typography.body-md}" + rounded: "{rounded.lg}" + padding: 24px + timeline-pill-thinking: + backgroundColor: "{colors.timeline-thinking}" + textColor: "{colors.ink}" + typography: "{typography.caption-uppercase}" + rounded: "{rounded.pill}" + padding: 4px 10px + timeline-pill-grep: + backgroundColor: "{colors.timeline-grep}" + textColor: "{colors.ink}" + typography: "{typography.caption-uppercase}" + rounded: "{rounded.pill}" + padding: 4px 10px + timeline-pill-read: + backgroundColor: "{colors.timeline-read}" + textColor: "{colors.ink}" + typography: "{typography.caption-uppercase}" + rounded: "{rounded.pill}" + padding: 4px 10px + timeline-pill-edit: + backgroundColor: "{colors.timeline-edit}" + textColor: "{colors.ink}" + typography: "{typography.caption-uppercase}" + rounded: "{rounded.pill}" + padding: 4px 10px + timeline-pill-done: + backgroundColor: "{colors.timeline-done}" + textColor: "{colors.on-primary}" + typography: "{typography.caption-uppercase}" + rounded: "{rounded.pill}" + padding: 4px 10px + code-block: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + typography: "{typography.code}" + rounded: "{rounded.lg}" + padding: 20px + pricing-tier-card: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + typography: "{typography.body-md}" + rounded: "{rounded.lg}" + padding: 32px + pricing-tier-featured: + backgroundColor: "{colors.ink}" + textColor: "{colors.canvas}" + typography: "{typography.body-md}" + rounded: "{rounded.lg}" + padding: 32px + text-input: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.ink}" + typography: "{typography.body-md}" + rounded: "{rounded.md}" + padding: 12px 16px + height: 44px + badge-pill: + backgroundColor: "{colors.surface-strong}" + textColor: "{colors.ink}" + typography: "{typography.caption-uppercase}" + rounded: "{rounded.pill}" + padding: 4px 10px + cta-band: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + typography: "{typography.display-lg}" + padding: 96px + testimonial-card: + backgroundColor: "{colors.surface-card}" + textColor: "{colors.body}" + typography: "{typography.body-md}" + rounded: "{rounded.lg}" + padding: 24px + footer: + backgroundColor: "{colors.canvas}" + textColor: "{colors.body}" + typography: "{typography.body-sm}" + padding: 64px 48px + footer-link: + backgroundColor: transparent + textColor: "{colors.body}" + typography: "{typography.body-sm}" +--- + +## Overview + +Cursor's marketing site reads as a quietly-confident developer brand that believes in editorial calm over IDE-darkness. The base canvas is **warm cream** (`{colors.canvas}` — #f7f7f4) holding warm near-black ink (`{colors.ink}` — #26251e) for body and display alike. The single brand voltage is **Cursor Orange** (`{colors.primary}` — #f54e00) reserved for primary CTAs and the wordmark — used scarcely. + +Type runs **CursorGothic** as the single sans family. Display sits at weight 400 with negative letter-spacing — a magazine-editorial voice rather than tech-bombastic. JetBrains Mono carries every code surface (and code surfaces are roughly half the page). + +The brand's strongest visual signature is the **AI-timeline pill palette**: five pastel pills (peach `{colors.timeline-thinking}`, mint `{colors.timeline-grep}`, blue `{colors.timeline-read}`, lavender `{colors.timeline-edit}`, gold `{colors.timeline-done}`) marking AI-action stages inside in-product timeline visualizations. Used only in product UI — never as system action colors. + +**Key Characteristics:** +- Warm cream canvas, not white. Ink is warm (#26251e), not pure black. +- Single CTA color: `{colors.primary}` (Cursor Orange #f54e00). Used scarcely. +- Display weight stays at 400 — never bold. Magazine voice. +- AI timeline pastels: 5 dedicated tokens for in-product agent action stages. +- Compact 8px CTA radius — developer dialect. +- Hairline-only depth; no drop shadows. +- 80px section rhythm. + +## Colors + +### Brand & Accent +- **Cursor Orange** (`{colors.primary}` — #f54e00): Primary CTA pills, wordmark, hero accent. Used scarcely. +- **Cursor Orange Active** (`{colors.primary-active}` — #d04200): Press state. + +### Surface +- **Canvas** (`{colors.canvas}` — #f7f7f4): Warm cream page floor. +- **Canvas Soft** (`{colors.canvas-soft}` — #fafaf7): IDE-pane background inside mockups. +- **Surface Card** (`{colors.surface-card}` — #ffffff): Pure white card surface — slight contrast against the cream canvas. +- **Surface Strong** (`{colors.surface-strong}` — #e6e5e0): Badges, tag pills. + +### Hairlines +- **Hairline** (`{colors.hairline}` — #e6e5e0): 1px divider. +- **Hairline Soft** (`{colors.hairline-soft}` — #efeee8): Lighter divider. +- **Hairline Strong** (`{colors.hairline-strong}` — #cfcdc4): Stronger panel outline. + +### Text +- **Ink** (`{colors.ink}` — #26251e): Display, body emphasis. Warm near-black. +- **Body** (`{colors.body}` — #5a5852): Default running-text. +- **Body Strong** (`{colors.body-strong}` — #26251e): Same as ink. +- **Muted** (`{colors.muted}` — #807d72): Sub-titles. +- **Muted Soft** (`{colors.muted-soft}` — #a09c92): Disabled text. +- **On Primary** (`{colors.on-primary}` — #ffffff): White text on Cursor Orange. + +### Timeline (AI-action signature) +- **Thinking** (`{colors.timeline-thinking}` — #dfa88f): Peach. Used inside in-product agent timeline only. +- **Grep** (`{colors.timeline-grep}` — #9fc9a2): Mint. +- **Read** (`{colors.timeline-read}` — #9fbbe0): Pastel blue. +- **Edit** (`{colors.timeline-edit}` — #c0a8dd): Lavender. +- **Done** (`{colors.timeline-done}` — #c08532): Warm gold. + +### Semantic +- **Success** (`{colors.semantic-success}` — #1f8a65): Confirmation indicators. +- **Error** (`{colors.semantic-error}` — #cf2d56): Validation errors. + +## Typography + +### Font Family +**CursorGothic** is the licensed display + body family. Fallback: `system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif`. Code surfaces switch to **JetBrains Mono**. + +### Hierarchy + +| Token | Size | Weight | Line Height | Letter Spacing | Use | +|---|---|---|---|---|---| +| `{typography.display-mega}` | 72px | 400 | 1.1 | -2.16px | Homepage hero h1 | +| `{typography.display-lg}` | 36px | 400 | 1.2 | -0.72px | Section heads | +| `{typography.display-md}` | 26px | 400 | 1.25 | -0.325px | Sub-section heads | +| `{typography.display-sm}` | 22px | 400 | 1.3 | -0.11px | Card group titles | +| `{typography.title-md}` | 18px | 600 | 1.4 | 0 | Component titles | +| `{typography.title-sm}` | 16px | 600 | 1.4 | 0 | List labels | +| `{typography.body-md}` | 16px | 400 | 1.5 | 0 | Default body | +| `{typography.body-tracked}` | 16px | 400 | 1.5 | 0.08px | Tracked editorial body | +| `{typography.body-sm}` | 14px | 400 | 1.5 | 0 | Footer body | +| `{typography.caption}` | 13px | 400 | 1.4 | 0 | Photo captions | +| `{typography.caption-uppercase}` | 11px | 600 | 1.4 | 0.88px | Section labels, timeline pill labels | +| `{typography.code}` | 13px | 400 | 1.5 | 0 | Code blocks — JetBrains Mono | +| `{typography.button}` | 14px | 500 | 1.0 | 0 | CTA pill labels | +| `{typography.nav-link}` | 14px | 500 | 1.4 | 0 | Top-nav menu | + +### Principles +- **Display weight stays at 400.** Magazine voice, never bold. +- **Negative letter-spacing on display only.** -0.11px to -2.16px tracking. +- **JetBrains Mono on every code surface.** + +### Note on Font Substitutes +CursorGothic is licensed. Open-source substitute: **Inter** at weight 400 with letter-spacing -1.5%. Or **GT Sectra** for a more editorial feel. + +## Layout + +### Spacing System +- **Base unit:** 4px. +- **Tokens:** `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.base}` 16px · `{spacing.md}` 20px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 80px. +- **Section padding:** 80px. + +### Grid & Container +- Max content width: ~1200px. +- Editorial body: 12-column grid. +- Feature card grids: 2-up at desktop for splits, 3-up for benefits. +- Footer: 5-column at desktop. + +### Whitespace Philosophy +Generous editorial pacing — closer to a print magazine than a tech site. The cream canvas has plenty of breathing room; cards within bands sit close (16-24px gap). + +## Elevation & Depth + +The system uses **hairline-only depth**. No drop shadows, no elevation tiers. Cards float above the canvas via 1px hairlines and the slight white-on-cream contrast. + +| Level | Treatment | Use | +|---|---|---| +| Flat (canvas) | `{colors.canvas}` (#f7f7f4) | Body bands, footer | +| Card | `{colors.surface-card}` (#ffffff) | Content cards | +| Hairline border | 1px `{colors.hairline}` | Card outlines, dividers | +| IDE pane | `{colors.canvas-soft}` (#fafaf7) | Inside IDE mockup cards | + +### Decorative Depth +- **IDE-mockup cards** are the only "elevated" element. White card on cream canvas with internal pane structure mimicking the actual Cursor editor. +- **Timeline pastel pills** add chromatic depth without surface elevation. + +## Shapes + +### Border Radius Scale + +| Token | Value | Use | +|---|---|---| +| `{rounded.none}` | 0px | Reserved | +| `{rounded.xs}` | 4px | Inline tags | +| `{rounded.sm}` | 6px | Compact rows | +| `{rounded.md}` | 8px | CTA buttons, form inputs | +| `{rounded.lg}` | 12px | Cards, IDE panes | +| `{rounded.xl}` | 16px | Larger feature cards (rare) | +| `{rounded.pill}` | 9999px | Timeline pills, badges | +| `{rounded.full}` | 9999px | Avatars (rare) | + +## Components + +### Top Navigation + +**`top-nav`** — Background `{colors.canvas}`, text `{colors.ink}`, height 64px. Layout: Cursor wordmark left, primary horizontal menu (Pricing / Features / Enterprise / Blog / Forum / Careers), Sign In + Download primary CTA right. + +### Buttons + +**`button-primary`** — The signature Cursor Orange CTA. Background `{colors.primary}`, text `{colors.on-primary}`, type `{typography.button}` (14px / 500), padding 10px × 18px, height 40px, rounded `{rounded.md}` (8px). + +**`button-primary-active`** — Press state. Background `{colors.primary-active}`. + +**`button-secondary`** — White card pill on cream canvas. Background `{colors.surface-card}`, text `{colors.ink}`, 1px `{colors.hairline-strong}` border. + +**`button-tertiary-text`** — Inline ink text link. + +**`button-download`** — Larger ink-canvas CTA. Background `{colors.ink}`, text `{colors.canvas}`, padding 12px × 20px, height 44px. Used for "Download for macOS" type CTAs. + +### Hero & IDE Mockups + +**`hero-band`** — Background `{colors.canvas}`, full-width display headline in `{typography.display-mega}` (72px / 400 / -2.16px), subhead in `{typography.body-md}`, two CTAs (`button-download` + `button-tertiary-text`), and a centered IDE-mockup card below the hero copy. + +**`ide-mockup-card`** — A white card containing a multi-pane IDE mockup (sidebar + main editor + chat panel + terminal). Background `{colors.surface-card}`, rounded `{rounded.lg}` (12px), 1px `{colors.hairline}` border, no padding (panes fill the card edge-to-edge). + +**`ide-pane`** — Individual IDE pane inside the mockup. Background `{colors.canvas-soft}`, text `{colors.body}` in `{typography.code}` (JetBrains Mono 13px), rounded `{rounded.md}` (8px), padding 16px. + +### Cards + +**`feature-card`** — Background `{colors.surface-card}`, text `{colors.ink}`, type `{typography.title-md}`, rounded `{rounded.lg}`, padding 24px. 1px `{colors.hairline}` border. + +**`comparison-card`** — Side-by-side "Cursor vs other tools" card. Same surface and rounding; internally split into 2 columns. + +**`testimonial-card`** — Quote card. Background `{colors.surface-card}`, text `{colors.body}`, rounded `{rounded.lg}`, padding 24px. + +### AI Timeline (signature) + +**`timeline-pill-thinking`** — Peach pill. Background `{colors.timeline-thinking}`, text `{colors.ink}`, type `{typography.caption-uppercase}` (11px / 600 / 0.88px tracking, uppercase), rounded `{rounded.pill}`, padding 4px × 10px. Marks "Thinking" stage in product timeline. + +**`timeline-pill-grep`** — Mint pill. Same shape, background `{colors.timeline-grep}`. Marks "Grepping" stage. + +**`timeline-pill-read`** — Pastel-blue pill. Background `{colors.timeline-read}`. Marks "Reading" stage. + +**`timeline-pill-edit`** — Lavender pill. Background `{colors.timeline-edit}`. Marks "Editing" stage. + +**`timeline-pill-done`** — Gold pill. Background `{colors.timeline-done}`, text `{colors.on-primary}` white. Marks "Done" stage. + +### Code + +**`code-block`** — Inline code block. Background `{colors.surface-card}`, text `{colors.ink}` in `{typography.code}`, rounded `{rounded.lg}`, padding 20px, 1px `{colors.hairline}` border. + +### Pricing + +**`pricing-tier-card`** — Background `{colors.surface-card}`, rounded `{rounded.lg}`, padding 32px, 1px `{colors.hairline}` border. + +**`pricing-tier-featured`** — Featured tier inverts to ink. Background `{colors.ink}`, text `{colors.canvas}`. Same shape, dark inversion signals "highlighted" without colored ribbon. + +### Forms & Tags + +**`text-input`** — Background `{colors.surface-card}`, text `{colors.ink}`, rounded `{rounded.md}` (8px), padding 12px × 16px, height 44px. + +**`badge-pill`** — Small uppercase pill. Background `{colors.surface-strong}`, text `{colors.ink}`, type `{typography.caption-uppercase}`, rounded `{rounded.pill}`, padding 4px × 10px. + +### CTA / Footer + +**`cta-band`** — Pre-footer "Try Cursor now" band. Background `{colors.canvas}`, centered display headline in `{typography.display-lg}`, single Cursor Orange CTA. 96px vertical padding. + +**`footer`** — Closing footer. Background `{colors.canvas}`, text `{colors.body}`. 5-column link list. 64×48px padding. + +**`footer-link`** — Background transparent, text `{colors.body}`, type `{typography.body-sm}`. + +## Do's and Don'ts + +### Do +- Reserve `{colors.primary}` (Cursor Orange) for primary CTAs and brand wordmark. +- Keep display weight at 400. The editorial voice depends on this. +- Use the cream `{colors.canvas}` page floor — never pure white. +- Render every code surface (inline, blocks, IDE panes) in JetBrains Mono. +- Use timeline pastels only inside in-product agent visualizations — never as system action colors. + +### Don't +- Don't introduce a secondary brand action color. Cursor Orange is the only one. +- Don't drop display to bold weights (700+). Magazine voice depends on 400. +- Don't add drop shadows. Hairlines + ink-on-cream contrast carry the depth. +- Don't use timeline pastels on non-timeline UI. They're scoped to the agent timeline only. +- Don't extract a CTA color from a third-party widget (cookie consent, OneTrust). The brand's CTA is what appears on actual product CTAs. + +## Responsive Behavior + +### Breakpoints + +| Name | Width | Key Changes | +|---|---|---| +| Mobile | < 640px | Hero h1 72→32px; IDE mockup collapses to single pane preview; feature grid 1-up; nav hamburger. | +| Tablet | 640–1024px | Hero h1 56px; IDE mockup compresses; feature grid 2-up. | +| Desktop | 1024–1280px | Full hero h1 72px; full multi-pane IDE mockup; feature grid 3-up. | +| Wide | > 1280px | Content caps at 1200px. | + +### Touch Targets +- Primary CTA at 40px height — at WCAG AA, padded for AAA. +- Download CTA at 44px — at AAA. + +### Collapsing Strategy +- Top nav switches to hamburger below 768px. +- IDE mockup multi-pane collapses to a single primary pane preview on mobile. +- Feature grid: 3-up → 2-up → 1-up. + +## Iteration Guide + +1. Focus on a single component at a time. +2. CTAs default to `{rounded.md}` (8px). Cards use `{rounded.lg}` (12px). +3. Variants live as separate entries inside `components:`. +4. Use `{token.refs}` everywhere — never inline hex. +5. Hover state never documented. +6. CursorGothic 400 for display, 400/500/600 for body. JetBrains Mono on every code surface. +7. Cursor Orange stays scarce. +8. Timeline pastels stay scoped to in-product agent visualizations. + +## Known Gaps + +- CursorGothic is a licensed typeface; Inter is the substitute. +- Animation timings (timeline pill entrance, IDE pane reveal) out of scope. +- In-app surfaces (code editor, chat panel, agent timeline) only partially captured via marketing IDE mockups. +- Form validation states beyond focus not visible on captured surfaces. diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index dc317f7..7c975c8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import "./App.css"; import "./chat.css"; import "./profile.css"; +import "./cursor-theme.css"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; import Repository from "./pages/Repository"; diff --git a/frontend/src/cursor-theme.css b/frontend/src/cursor-theme.css new file mode 100644 index 0000000..e5899ef --- /dev/null +++ b/frontend/src/cursor-theme.css @@ -0,0 +1,873 @@ +:root { + --paper: #0d1117; + --surface: #161b22; + --surface-soft: #21262d; + --ink: #f0f6fc; + --ink-soft: #c9d1d9; + --muted: #8b949e; + --line: #30363d; + --line-strong: #484f58; + --coral: #58a6ff; + --coral-dark: #79c0ff; + --success: #3fb950; + --danger: #f85149; + --mono: "DM Mono", "JetBrains Mono", Consolas, monospace; + --sans: "Manrope", "Helvetica Neue", Arial, sans-serif; +} + +html { + background: var(--paper); +} + +body { + background: var(--paper); + color: var(--ink); + font-family: var(--sans); +} + +button, +input, +select { + font-family: inherit; +} + +.shell { + width: min(1160px, calc(100% - 64px)); + padding: 32px 0 88px; +} + +.dashboard-shell { + overflow: hidden; +} + +.dashboard-hero { + position: relative; +} + +.dashboard-hero::after { + position: absolute; + right: 6%; + bottom: 52px; + width: 96px; + height: 1px; + background: var(--coral); + content: ""; + opacity: .7; +} + +.dashboard-hero h2 span { + color: var(--coral); +} + +.dashboard-stats, +.activity-band, +.profile-strip, +.chat-panel, +.section-heading, +.repo-toolbar, +.repo-grid { + animation: dashboard-rise .55s ease both; +} + +.dashboard-stats { + animation-delay: .08s; +} + +.activity-band { + animation-delay: .14s; +} + +.chat-panel { + animation-delay: .2s; +} + +.section-heading, +.repo-toolbar { + animation-delay: .26s; +} + +.repo-grid { + animation-delay: .32s; +} + +@keyframes dashboard-rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.topbar { + min-height: 64px; + padding-bottom: 20px; + border-bottom: 1px solid var(--line); +} + +.brand-mark { + width: 38px; + height: 38px; + margin-right: -8px; + border-radius: 8px; + background: var(--coral); + color: #fff; + font: 600 12px var(--mono); + letter-spacing: 0; +} + +.topbar h1 { + font-size: 16px; + font-weight: 600; +} + +.eyebrow { + margin-bottom: 8px; + color: var(--muted); + font: 600 10px/1.4 var(--mono); + letter-spacing: .08em; +} + +.accent { + color: var(--coral); +} + +.intro-row { + align-items: end; + padding: 88px 0 56px; +} + +.intro-row h2, +.detail-header h1 { + max-width: 720px; + margin: 0 0 16px; + font-size: clamp(38px, 5.5vw, 72px); + font-weight: 400; + line-height: 1.08; + letter-spacing: 0; +} + +.lede { + max-width: 580px; + color: var(--ink-soft); + font-size: 15px; + line-height: 1.7; +} + +.primary-button, +.outline-button, +.back-button { + min-height: 42px; + border-radius: 6px; + padding: 11px 18px; + font: 600 12px/1 var(--sans); + letter-spacing: 0; + transition: background-color .18s ease, border-color .18s ease, color .18s ease, transform .18s ease; +} + +.primary-button { + background: var(--coral); + color: #fff; + box-shadow: none; +} + +.primary-button:hover:not(:disabled) { + background: var(--coral-dark); + transform: translateY(-1px); +} + +.primary-button:disabled { + background: var(--line-strong); +} + +.outline-button { + border: 1px solid var(--line-strong); + background: var(--surface); + color: var(--ink); +} + +.outline-button:hover { + border-color: var(--ink); + background: var(--surface-soft); +} + +.back-button { + margin-bottom: 40px; + padding: 0; + border-radius: 0; + background: transparent; + color: var(--muted); +} + +.back-button:hover { + color: var(--coral); +} + +.stats-grid { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); +} + +.stat { + min-height: 126px; + padding: 22px 24px; + border-right: 1px solid var(--line); +} + +.stat-label, +.stat-detail { + color: var(--muted); + font: 11px/1.4 var(--mono); +} + +.stat strong { + margin: 2px 0; + color: var(--ink); + font-size: 30px; + font-weight: 400; + letter-spacing: 0; +} + +.activity-band { + margin: 28px 0 0; + border-radius: 8px; + background: var(--ink); + box-shadow: none; +} + +.activity-spark { + border-color: rgba(255, 255, 255, .2); + color: var(--coral); +} + +.activity-intro h2 { + font-weight: 500; + letter-spacing: 0; +} + +.activity-metric strong { + font-weight: 400; + letter-spacing: 0; +} + +.activity-link:hover { + color: #ff9b72; +} + +.profile-strip { + margin-top: 1px; + padding: 24px 0; + border-bottom: 1px solid var(--line); +} + +.profile-strip strong { + font-weight: 600; + overflow-wrap: anywhere; +} + +.section-heading { + margin: 76px 0 22px; +} + +.section-heading h2, +.panel h2, +.chat-heading h2 { + font-weight: 500; + letter-spacing: 0; +} + +.muted { + color: var(--muted); + line-height: 1.6; +} + +.repo-toolbar { + align-items: stretch; + margin-bottom: 20px; +} + +.search-field { + width: 280px; + border-radius: 6px; + border-color: var(--line-strong); + background: var(--surface); +} + +.search-field:focus-within { + border-color: var(--coral); + box-shadow: 0 0 0 3px rgba(245, 78, 0, .1); +} + +.filter-pills { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.filter-pills button { + border: 1px solid var(--line); + border-radius: 999px; + padding: 8px 12px; + background: transparent; + color: var(--muted); + font: 11px var(--mono); + cursor: pointer; +} + +.filter-pills button:hover, +.filter-pills button.active { + border-color: var(--coral); + background: #fff4ef; + color: var(--coral); +} + +.repo-grid { + gap: 12px; +} + +.repo-card { + position: relative; + min-height: 216px; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 8px; + padding: 22px; + background: var(--surface); + color: var(--ink); + text-align: left; + cursor: pointer; + transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease; +} + +.repo-card:nth-child(2) { animation-delay: .04s; } +.repo-card:nth-child(3) { animation-delay: .08s; } +.repo-card:nth-child(4) { animation-delay: .12s; } + +.repo-card::before { + position: absolute; + inset: 0 auto 0 0; + width: 3px; + background: var(--coral); + content: ""; + opacity: 0; + transition: opacity .18s ease; +} + +.repo-card:hover { + border-color: var(--line-strong); + box-shadow: 0 10px 24px rgba(38, 37, 30, .07); + transform: translateY(-2px); +} + +.repo-card:hover::before { + opacity: 1; +} + +.repo-card-head { + margin-bottom: 24px; +} + +.repo-icon { + width: 34px; + height: 34px; + border-radius: 6px; + background: var(--ink); + color: var(--paper); + font: 11px var(--mono); +} + +.repo-card .arrow { + color: var(--coral); +} + +.repo-card h3 { + margin: 0 0 9px; + font-size: 18px; + font-weight: 600; + overflow-wrap: anywhere; +} + +.repo-card p { + min-height: 45px; + margin: 0 0 20px; + color: var(--ink-soft); + font-size: 13px; + line-height: 1.55; +} + +.repo-meta { + border-top: 1px solid var(--line); + padding-top: 13px; + color: var(--muted); + font: 10px var(--mono); +} + +.detail-header { + align-items: end; + padding-bottom: 52px; +} + +.detail-shell .detail-header h1 { + font-size: clamp(42px, 6vw, 80px); +} + +.detail-stats { + margin-bottom: 28px; +} + +.detail-grid { + gap: 12px; +} + +.panel { + min-height: 178px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 24px; + background: var(--surface); +} + +.panel-title { + align-items: start; +} + +.status-dot { + color: var(--success); + font: 10px var(--mono); + white-space: nowrap; +} + +.pill-row { + gap: 6px; + margin: 22px 0; +} + +.pill { + border: 1px solid var(--line); + border-radius: 999px; + padding: 5px 9px; + background: var(--surface-soft); + color: var(--ink-soft); + font: 10px var(--mono); +} + +.architecture-line { + display: flex; + align-items: center; + gap: 12px; + margin: 26px 0 14px; + color: var(--coral); + font: 12px var(--mono); +} + +.architecture-line strong { + color: var(--ink); + font-weight: 500; +} + +.file-list, +.source-list { + margin-top: 18px; +} + +.file-list li, +.source-list > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + border-top: 1px solid var(--line); + padding: 11px 0; +} + +.file-list li span, +.source-list > div span { + color: var(--muted); + font: 10px var(--mono); + text-align: right; +} + +code { + color: var(--ink); + font-family: var(--mono); + font-size: 11px; + overflow-wrap: anywhere; +} + +.notice { + margin: 20px 0; + border-radius: 6px; + padding: 13px 15px; + font: 12px var(--mono); +} + +.notice.error { + border: 1px solid #f0b7c7; + background: #fff4f6; + color: var(--danger); +} + +.loading-state { + display: grid; + min-height: 100vh; + place-items: center; + background: var(--paper); + color: var(--muted); + font: 12px var(--mono); +} + +.login-shell { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(300px, .8fr); + align-items: center; + gap: clamp(56px, 10vw, 148px); + min-height: 100vh; + max-width: 1160px; +} + +.login-copy { + max-width: 680px; +} + +.login-shell h1 { + max-width: 620px; + margin: 0 0 20px; + font-size: clamp(48px, 6.2vw, 82px); + font-weight: 400; + line-height: 1.02; + letter-spacing: 0; +} + +.login-shell .lede { + margin-bottom: 30px; + font-size: 17px; +} + +.login-flow { + position: relative; + align-self: center; + border-top: 1px solid var(--line-strong); + border-bottom: 1px solid var(--line-strong); + padding: 22px 0 18px; + animation: reveal-up .7s .16s both; +} + +.flow-heading, +.flow-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; +} + +.flow-heading .eyebrow { + margin: 0; +} + +.flow-live { + display: flex; + align-items: center; + gap: 7px; + color: var(--success); + font: 10px var(--mono); +} + +.flow-live i { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--success); + box-shadow: 0 0 0 4px rgba(31, 138, 101, .12); +} + +.flow-line { + position: absolute; + top: 67px; + bottom: 55px; + left: 17px; + width: 1px; + background: var(--line); +} + +.flow-step { + position: relative; + display: grid; + grid-template-columns: 35px 1fr; + gap: 14px; + align-items: start; + padding: 24px 0 0; +} + +.flow-step > span { + z-index: 1; + display: grid; + width: 35px; + height: 35px; + place-items: center; + border: 1px solid var(--line-strong); + border-radius: 50%; + background: var(--paper); + color: var(--muted); + font: 10px var(--mono); +} + +.flow-step-active > span { + border-color: var(--coral); + background: var(--coral); + color: #fff; +} + +.login-flow { + background: rgba(22, 27, 34, .58); + box-shadow: 0 24px 80px rgba(1, 4, 9, .24); + padding-right: 22px; + padding-left: 22px; +} + +.flow-step > span { + background: var(--paper); +} + +.profile-menu { + background: var(--surface); + box-shadow: 0 18px 42px rgba(1, 4, 9, .42); +} + +.profile-menu a:hover { + background: var(--surface-soft); +} + +.profile-menu > button:hover { + background: rgba(248, 81, 73, .12); +} + +.chat-panel { + background: var(--surface); + box-shadow: 0 16px 44px rgba(1, 4, 9, .18); +} + +.suggestions button, +.provider-control select, +.chat-form input { + background: var(--surface-soft); + color: var(--ink); +} + +.chat-answer { + background: var(--surface-soft); +} + +.answer-content code { + background: var(--surface); +} + +.answer-content pre { + background: #010409; +} + +.flow-step strong { + display: block; + padding-top: 3px; + font-size: 14px; + font-weight: 600; +} + +.flow-step p { + margin: 6px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.5; +} + +.flow-footer { + margin: 28px 0 0 49px; + color: var(--muted); + font: 9px var(--mono); + letter-spacing: .08em; +} + +@keyframes reveal-up { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +.login-shell > .login-copy { + animation: reveal-up .7s both; +} + +:focus-visible { + outline: 3px solid rgba(245, 78, 0, .35); + outline-offset: 3px; +} + +@media (max-width: 760px) { + .shell { + width: min(100% - 32px, 600px); + padding-top: 20px; + } + + .topbar, + .intro-row, + .detail-header { + align-items: flex-start; + flex-direction: column; + } + + .topbar { + flex-direction: row; + align-items: center; + } + + .topbar > div:first-of-type { + margin-right: 0; + } + + .topbar .profile-area { + margin-left: auto; + } + + .intro-row { + padding: 64px 0 40px; + } + + .dashboard-hero::after { + right: 0; + bottom: 34px; + width: 54px; + } + + .intro-row .primary-button, + .detail-header .outline-button { + width: 100%; + } + + .login-shell { + display: block; + min-height: 0; + padding-top: 18vh; + } + + .login-flow { + padding-right: 0; + padding-left: 0; + background: transparent; + box-shadow: none; + } + + .login-flow { + margin-top: 72px; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .stat:nth-child(2) { + border-right: 0; + } + + .stat:nth-child(-n + 2) { + border-bottom: 1px solid var(--line); + } + + .activity-band { + grid-template-columns: 1fr 1fr; + } + + .activity-intro, + .activity-link { + grid-column: 1 / -1; + } + + .activity-metric { + border-left: 0; + padding-left: 0; + } + + .profile-strip { + grid-template-columns: 1fr; + gap: 16px; + } + + .repo-toolbar { + align-items: stretch; + flex-direction: column; + } + + .search-field { + width: 100%; + } + + .detail-header { + padding-bottom: 36px; + } + + .detail-grid { + grid-template-columns: 1fr; + } + + .wide-panel { + grid-column: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: .01ms !important; + } +} + +@media (max-width: 480px) { + .topbar .eyebrow, + .topbar h1 { + display: none; + } + + .user-chip span { + min-width: 0; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .stat, + .stat:nth-child(2) { + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .stat:last-child { + border-bottom: 0; + } + + .activity-band { + grid-template-columns: 1fr; + } + + .activity-metric { + border-top: 1px solid rgba(255, 255, 255, .16); + padding-top: 13px; + } + + .file-list li, + .source-list > div { + align-items: flex-start; + flex-direction: column; + gap: 5px; + } + + .file-list li span, + .source-list > div span { + text-align: left; + } +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index ed6a9ee..12db324 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -91,7 +91,7 @@ function Dashboard({ onOpenRepository, onSignedOut }) { }); return ( -
+
GA
@@ -138,10 +138,12 @@ function Dashboard({ onOpenRepository, onSignedOut }) {
)}
-
+

Your workspace

-

A clearer map of how you build.

+

+ A clearer map of how you build. +

Analyze repository structure, technologies, activity, and architecture from one quiet workspace. @@ -156,7 +158,10 @@ function Dashboard({ onOpenRepository, onSignedOut }) {

{error &&
{error}
} -
+
-
+
{filteredRepositories.map((repo) => ( +
+

Repository intelligence

+

See the shape of your code.

+

+ Connect GitHub to map your repositories, understand the architecture, + and find the projects where your work is moving fastest. +

+ +
+
); } diff --git a/frontend/src/pages/Repository.jsx b/frontend/src/pages/Repository.jsx index 60940b9..453c926 100644 --- a/frontend/src/pages/Repository.jsx +++ b/frontend/src/pages/Repository.jsx @@ -97,6 +97,30 @@ function Repository({ repository, onBack }) { → {analysis.architecture.backend || "Source"} +

+ {analysis.architecture.analyzed_source_files || 0} source files ·{" "} + {analysis.architecture.module_relationships || 0} detected module relationships +

+ {analysis.architecture.entry_points?.length > 0 && ( +
+ {analysis.architecture.entry_points.map((file) => ( +
+ {file} + entry point +
+ ))} +
+ )} + {analysis.architecture.module_graph?.length > 0 && ( +
+ {analysis.architecture.module_graph.slice(0, 12).map((edge) => ( +
+ {edge.from} + → {edge.to} +
+ ))} +
+ )}

Technologies