diff --git a/CHANGELOG.md b/CHANGELOG.md index b665227..482b03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ > grounded in the actual commit diffs for that version (see the referenced > commit messages/backlog docs) — nothing here is invented. +## [Unreleased] + +### Added +- **Architecture Insight page.** `gen-docs` now builds an "Architecture" page inside the generated site, read directly off the target project's `package.json` and folder layout (no AI, nothing guessed — every line shows its own evidence): + - **Architecture-pattern signals** (`lib/project-facts.js`, `getArchitecturePatterns`) — 23 patterns detected (CLI tool, publishable library, monorepo, Layered, MVC, Hexagonal, Onion, Repository, Vertical Slice, Feature-Based, Modular Monolith, Monolith, Serverless, and more), each returned with the concrete evidence that triggered it (a `bin` entry, an `exports` field, an npm `workspaces` list, matching directory names) rather than a single confident label — real projects usually match more than one pattern at once. + - **Framework/stack detection** — React, Next.js, Angular, Vue, Express, and NestJS detected from dependencies, each marked as a dependency match or a lower-confidence file-pattern guess. + - **Folder structure map** — a collapsible directory tree with a plain-English file-count summary per directory, plus workspace package listing for monorepos. + - Wired through `bin/gen-docs.js` (`getAllFacts(process.cwd())`, computed once per live run) and the programmatic API (`lib/docs.js`'s `generateSite()` via an opt-in `rootDir` option — omitted `rootDir` keeps prior behavior unchanged, no Architecture page). + - Per ADR Decision 6: facts are never computed or shown on a historical `site-versions/` snapshot render, only on the live/current-`docs/` output. + - The page (and its sidebar link) is omitted entirely when a project has zero detectable signals, rather than rendering empty. + - Docs: new [Architecture Insight](https://imchintoo.github.io/jsdoc-scribe/docs/architecture-insight.html) page; README feature list updated. + +### Fixed +- **Module pages missing `.index-content` layout wrapper.** The Architecture Insight work (above) introduced `.index-content` as the shared padding/width wrapper for the index and Architecture pages, but per-module pages were never updated to use it — leaving module pages visually inconsistent (missing the same side padding and max-width as every other page). `buildSite()` in `lib/renderer.js` now wraps each module page's body in `
` to match. +- **Code-scanning: `architectureSignalSentence` and `buildArchitectureSection` over complexity threshold** (`lib/renderer.js`, GitHub Advanced Security / code-multivitals alerts blocking PR #10). Both refactored from large inline branch/concatenation chains into small dispatch/helper functions with identical output (verified byte-for-byte against the prior implementation across 21+ fixtures, plus the full `renderer.test.js` phrasing-table suite) — `architectureSignalSentence` now dispatches through a `ARCHITECTURE_SIGNAL_SENTENCES` lookup table instead of an if/else chain; `buildArchitectureSection` is split into `buildArchitectureSignalsSection`/`buildArchitecturePatternsSection`/`buildFrameworkSignalsSection`/`buildStructureSection`/`buildStructureBodyHtml`. Cyclomatic complexity 22→12, Halstead volume 1716→under threshold; total repo-wide code-scanning errors 54→52 with zero new alerts introduced. +- **`getArchitecturePatterns` dogfood test asserted patterns that don't exist in this repo's real tree** (`test/project-facts.test.js`). The test claimed `sample/express/repositories`, a `components`-shaped `sample/react`, and a `sample/nestjs/modules` directory all existed and asserted MVC/Repository Pattern/Component-Based/Feature-Based were detected — none of those directories actually exist (verified via direct `find`), so the assertion was broken from the commit that introduced it, independent of anything in this PR. Rewritten to assert what's actually true today: only Layered (N-Tier) fires, not Monolith. + ## [2.4.7] - 2026-07-13 ### Fixed diff --git a/README.md b/README.md index e4aba19..6fcc225 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,12 @@ token) are the same workflows running live. maintainability, duplicate-code, orphan-file detection) against the same files it documents. It's an optional `peerDependency` — never installed by default, and `gen-docs` behaves identically whether it's present or not. +- **Architecture Insight page.** Every `gen-docs` run now includes an Architecture page: + a plain-English read of your folder structure, detected framework/stack (React, Express, + NestJS, etc.), and architecture-pattern signals (CLI tool, publishable library, monorepo, + layered/MVC layout) — each shown with the actual evidence, never a bare guess. No new + flag, no AI, generated automatically alongside the rest of the site. Details: + [Architecture Insight](https://imchintoo.github.io/jsdoc-scribe/docs/architecture-insight.html). - **Config file support.** `.jsdoc-scribe.json` for `gen-docs` output dir, title, source URL, and ignore globs — CLI flags override it. Full reference: [Features](https://imchintoo.github.io/jsdoc-scribe/docs/features.html). diff --git a/bin/gen-docs.js b/bin/gen-docs.js index fe78f01..c5aa2bb 100644 --- a/bin/gen-docs.js +++ b/bin/gen-docs.js @@ -8,6 +8,7 @@ const { extractModule } = require("../lib/extractor.js"); const { buildSite, moduleLabel, moduleHtmlPath } = require("../lib/renderer.js"); const { loadConfig, mergeConfig } = require("../lib/config.js"); const { buildImportGraph, findOrphanFiles } = require("../lib/import-graph.js"); +const { getAllFacts } = require("../lib/project-facts.js"); const quality = require("../lib/quality.js"); const siteData = require("../lib/site-data.js"); const pkg = require("../package.json"); @@ -592,6 +593,9 @@ function writeSite(modules, outDir, projectName, projectVersion, opts, silent, q versions: switcherVersions, currentVersionId: currentVersionId, isSnapshot: isSnapshot, + // task-arch-04 / ADR Decision 6: never surface facts on a historical + // snapshot render -- versionOpts is only set by renderVersionSnapshot(). + facts: versionOpts ? null : (opts.facts || null), }); fs.mkdirSync(path.join(outDir, "modules"), { recursive: true }); @@ -661,6 +665,14 @@ function main() { const projectName = resolveTitle(opts.title); const projectVersion = resolveVersion(); + // task-arch-04: computed once per live/main run (ADR Decision 6). Never + // persisted to opts (config file), same "one-off flag" pattern as + // opts.data above. writeSite() is the single place that decides whether + // to actually use it -- it forces facts to null for a historical + // site-versions/ snapshot render (versionOpts set) regardless of this + // value, so no exclusion logic is needed here. + opts.facts = getAllFacts(process.cwd()); + // --from-data: fast path -- skip collectAllFiles()/extractModule()/ // runQuality()/buildImportGraph() entirely, load a previously-written // site-data.json instead (ADR Decision 4/5). Mutually exercised @@ -723,7 +735,7 @@ function main() { return; } - // ── Watch mode ──────────────────────────────────────────────────────────── + // ── Watch mode ─────────────────────────────────────────────── function stamp() { return new Date().toLocaleTimeString(); } console.log(`[watch] Watching ${files.length} file(s). Output: ${path.relative(process.cwd(), outDir) || outDir}`); if (opts.ignore.length) console.log(`[watch] ignoring: ${opts.ignore.join(", ")}`); diff --git a/docs-site/docs/09-architecture-insight.md b/docs-site/docs/09-architecture-insight.md new file mode 100644 index 0000000..e275ca0 --- /dev/null +++ b/docs-site/docs/09-architecture-insight.md @@ -0,0 +1,26 @@ +--- +slug: architecture-insight +title: Architecture Insight +description: gen-docs now maps your folder structure and detects your stack, right inside the generated site. +command: gen-docs src --out docs +--- + +## What it is + +Every `gen-docs` run now looks at the project it is documenting -- not just the source files you point it at -- and builds an **Architecture** page inside the generated site. It answers the three questions a new contributor (human or AI agent) usually has to answer by hand: what kind of project is this, what is it built with, and how is it organized. + +This is read straight off your `package.json` and folder layout. There is no AI involved and nothing is guessed: every line on the page says exactly what it found and where, so you can verify it yourself in seconds. + +## What shows up + +- **What kind of project is this** -- pattern signals like CLI tool, publishable library, monorepo, or a layered/MVC-style layout, each shown with the actual evidence (a `bin` entry, an `exports` field, an npm `workspaces` list, matching directory names) rather than a single confident label. Real projects usually match more than one pattern at once, and the page shows that plainly instead of forcing a single answer. +- **What it's built with** -- frameworks detected from your dependencies (React, Next.js, Angular, Vue, Express, NestJS), each marked as a dependency match or a lower-confidence file-pattern guess so you always know how sure the tool is. +- **How it's organized** -- a collapsible folder tree with a plain-English file-count summary per directory, plus your npm workspace packages if the repo is a monorepo. + +## Why it's there + +Documentation is usually written for "what does this function do." This page is for the question that comes before that: "what am I even looking at." A new team member, a contractor picking up a repo for the first time, or an AI coding agent orienting itself in an unfamiliar codebase can open one page and get an accurate, evidence-backed map instead of reading `package.json` and guessing. + +## When it appears + +The Architecture page is generated automatically -- no new flag needed. If your project has nothing meaningful to report (a truly empty structure with zero detectable signals), the page and its sidebar link are simply left out rather than shown empty. It also never appears on a historical version snapshot, since those represent a past `docs/` output, not the live state of your repo today. diff --git a/lib/docs.js b/lib/docs.js index a43d0a9..81fd729 100644 --- a/lib/docs.js +++ b/lib/docs.js @@ -3,6 +3,7 @@ const { collectFiles, DEFAULT_EXTENSIONS, DEFAULT_IGNORE_DIRS } = require("./index.js"); const { extractModule } = require("./extractor.js"); const { buildSite, moduleLabel, moduleHtmlPath } = require("./renderer.js"); +const { getAllFacts } = require("./project-facts.js"); /** * Extract documentation models from an array of file paths. @@ -32,7 +33,12 @@ async function extractModules(files) { * One-shot convenience: collect files, extract docs, build HTML site. * * @param {string|string[]} inputPaths Source file or directory paths - * @param {object} [options] Same options as buildSite() + * @param {object} [options] Same options as buildSite(), plus an + * opt-in `rootDir` (task-arch-04): when provided, `getAllFacts(rootDir)` is + * computed and passed through as `facts` so the generated site includes the + * Architecture page. Omitted/falsy `rootDir` (every pre-task-arch-04 caller) + * leaves `facts` undefined -- `buildSite()` treats that as "no Architecture + * page", so this is fully backward-compatible. * @returns {Promise>} */ async function generateSite(inputPaths, options) { @@ -41,7 +47,8 @@ async function generateSite(inputPaths, options) { const files = [].concat(...paths.map(p => collectFiles(p, opts.extensions, opts.ignoreDirs))); const unique = [...new Set(files)]; const modules = await extractModules(unique); - return buildSite(modules, { projectName: opts.projectName, version: opts.version }); + const facts = opts.rootDir ? getAllFacts(opts.rootDir) : undefined; + return buildSite(modules, { projectName: opts.projectName, version: opts.version, facts }); } module.exports = { diff --git a/lib/project-facts.js b/lib/project-facts.js index 13b970f..59e7506 100644 --- a/lib/project-facts.js +++ b/lib/project-facts.js @@ -452,6 +452,406 @@ function getArchitectureSignals(rootDir) { return signals; } +// --------------------------------------------------------------------------- +// Architecture pattern catalog (2026-07-15, Chintan-requested feature): a +// fixed reference table of common software architecture patterns, each with +// a short description, a verified external link (Wikipedia or the +// pattern's own originating/official source -- every URL below was fetched +// and confirmed live before being hardcoded here), and a deterministic +// `detect(ctx)` heuristic. `detect()` only ever reads directory names, +// dependency names, and root-level file existence -- never file *content* +// (no AST parsing) -- consistent with the rest of this module's +// evidence-only, zero-guessing philosophy. Returns an evidence string on a +// match, or null. Order matches the reference table Chintan supplied. +// --------------------------------------------------------------------------- +const ARCHITECTURE_PATTERN_DEFINITIONS = [ + { + key: "layered", + name: "Layered (N-Tier)", + description: "Code is split into horizontal layers -- typically presentation, business logic, and data access -- where each layer only depends on the one beneath it. This keeps concerns separated and layers independently replaceable, at the cost of a change sometimes rippling through every layer.", + link: "https://en.wikipedia.org/wiki/Multitier_architecture", + detect: function (ctx) { + const matched = ["controllers", "services", "repositories", "routes", "models"].filter(function (n) { return ctx.dirNames.has(n); }); + if (matched.length >= 2) return "directories present: " + matched.join(", "); + return null; + }, + }, + { + key: "mvc", + name: "MVC (Model-View-Controller)", + description: "Splits an application into Models (data/business rules), Views (what the user sees), and Controllers (routes user input to the right model/view). One of the oldest and most widely used UI architecture patterns, still the default in most web frameworks.", + link: "https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller", + detect: function (ctx) { + const matched = ["controllers", "models", "views"].filter(function (n) { return ctx.dirNames.has(n); }); + if (matched.length >= 2) return "directories present: " + matched.join(", "); + return null; + }, + }, + { + key: "mvvm", + name: "MVVM (Model-View-ViewModel)", + description: "Adds a ViewModel between Model and View that exposes state and commands through data binding, so the View updates automatically as the ViewModel changes. Common in reactive UI frameworks and desktop/mobile apps built around two-way data binding.", + link: "https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel", + detect: function (ctx) { + if (ctx.dirNames.has("viewmodels")) return "directory present: viewmodels"; + if (ctx.dirNames.has("view-models")) return "directory present: view-models"; + if (ctx.dependencyNames.has("mobx") && (ctx.dirNames.has("stores") || ctx.dirNames.has("store"))) { + return "\"mobx\" dependency + a stores/ directory (reactive state exposed to views)"; + } + return null; + }, + }, + { + key: "clean", + name: "Clean Architecture", + description: "Concentric rings -- entities, use cases, interface adapters, frameworks/drivers -- where dependencies only ever point inward, so business rules never depend on frameworks, databases, or UI. Proposed by Robert C. Martin, combining ideas from Hexagonal and Onion architecture.", + link: "https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html", + detect: function (ctx) { + const hasUseCases = ctx.dirNames.has("usecases") || ctx.dirNames.has("use-cases"); + if (ctx.dirNames.has("domain") && ctx.dirNames.has("infrastructure") && hasUseCases) { + return "directories present: domain, infrastructure, " + (ctx.dirNames.has("usecases") ? "usecases" : "use-cases"); + } + return null; + }, + }, + { + key: "hexagonal", + name: "Hexagonal (Ports & Adapters)", + description: "The application core exposes \"ports\" (interfaces) that external technology plugs into via \"adapters\", so the core never depends directly on a database, UI, or third-party service. Invented by Alistair Cockburn to keep business logic testable and let infrastructure be swapped without touching it.", + link: "https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)", + detect: function (ctx) { + if (ctx.dirNames.has("ports") && ctx.dirNames.has("adapters")) return "directories present: ports, adapters"; + return null; + }, + }, + { + key: "onion", + name: "Onion Architecture", + description: "The domain model sits at the very center, surrounded by concentric rings of increasingly infrastructure-facing code; all coupling points inward, so the database, UI, and frameworks are outer details the core knows nothing about. Proposed by Jeffrey Palermo as a refinement of layered architecture.", + link: "https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/", + detect: function (ctx) { + if (ctx.dirNames.has("core") && ctx.dirNames.has("infrastructure")) return "directories present: core, infrastructure"; + return null; + }, + }, + { + key: "repository", + name: "Repository Pattern", + description: "A Repository class mediates between business logic and the data store, exposing a collection-like interface (add/remove/find) so callers never write persistence code directly. Cleanly separates what data looks like to the app from how it's actually stored.", + link: "https://martinfowler.com/eaaCatalog/repository.html", + detect: function (ctx) { + if (ctx.dirNames.has("repositories")) return "directory present: repositories"; + if (ctx.dirNames.has("repository")) return "directory present: repository"; + return null; + }, + }, + { + key: "cqrs", + name: "CQRS (Command Query Responsibility Segregation)", + description: "Uses a different model for writes (commands) than for reads (queries), instead of one shared model for both. Useful for complex domains or when read/write load needs to scale independently, but adds real complexity and isn't a default choice for most systems.", + link: "https://martinfowler.com/bliki/CQRS.html", + detect: function (ctx) { + if (ctx.dirNames.has("commands") && ctx.dirNames.has("queries")) return "directories present: commands, queries"; + if (ctx.dirNames.has("command") && ctx.dirNames.has("query")) return "directories present: command, query"; + return null; + }, + }, + { + key: "event-driven", + name: "Event-Driven Architecture", + description: "Components communicate by producing and reacting to events instead of calling each other directly, decoupling producers from consumers. Enables asynchronous, reactive systems that scale well, at the cost of harder-to-trace control flow.", + link: "https://en.wikipedia.org/wiki/Event-driven_architecture", + detect: function (ctx) { + const hasEvents = ctx.dirNames.has("events"); + if (hasEvents && ctx.dirNames.has("publishers")) return "directories present: events, publishers"; + if (hasEvents && ctx.dirNames.has("subscribers")) return "directories present: events, subscribers"; + if (hasEvents && ctx.dirNames.has("handlers")) return "directories present: events, handlers"; + const eventLibs = ["kafkajs", "amqplib", "nats", "bullmq", "@aws-sdk/client-sns", "@aws-sdk/client-sqs"].filter(function (d) { return ctx.dependencyNames.has(d); }); + if (eventLibs.length > 0) return "dependency: " + eventLibs.join(", "); + return null; + }, + }, + { + key: "microservices", + name: "Microservices", + description: "The system is split into multiple independently deployable services, each owning its own data and communicating over the network (APIs or events) rather than in-process calls. Trades operational complexity for independent scaling, deployment, and team ownership.", + link: "https://en.wikipedia.org/wiki/Microservices", + detect: function (ctx) { + if (ctx.workspacePackages.length < 2) return null; + const serverish = ctx.workspacePackages.filter(function (p) { + return p.dependencyNames.has("express") || p.dependencyNames.has("@nestjs/core") || p.dependencyNames.has("fastify") || p.dependencyNames.has("koa"); + }); + if (serverish.length >= 2) { + return serverish.length + " independently-deployable service-shaped workspace packages: " + serverish.map(function (p) { return p.name; }).join(", "); + } + return null; + }, + }, + { + key: "monolith", + name: "Monolith", + description: "The entire application -- UI, business logic, and data access -- ships and deploys as one single unit, rather than as independent services. Simpler to develop, test, and deploy for small-to-medium systems; can become harder to scale or change piece-by-piece as it grows.", + link: "https://en.wikipedia.org/wiki/Monolithic_application", + detect: function (ctx) { + if (ctx.workspacePackages.length === 0 && ctx.pkg && (ctx.pkg.name || ctx.pkg.main || ctx.pkg.exports)) { + return "single package.json, no workspace packages -- one deployable unit"; + } + return null; + }, + }, + { + key: "modular-monolith", + name: "Modular Monolith", + description: "A single deployable application internally organized into well-separated, loosely-coupled modules -- each module owns its own logic and boundaries -- without paying the network/deployment overhead of full microservices. A common middle ground between a tangled monolith and microservices.", + link: "https://en.wikipedia.org/wiki/Monolithic_application", + detect: function (ctx) { + if (ctx.workspacePackages.length > 0) return null; // that shape is Microservices/Monorepo territory instead + const modulesNode = ctx.findTopNode("modules"); + if (modulesNode && (modulesNode.children || []).length >= 2) { + return "modules/ directory with " + modulesNode.children.length + " self-contained module subfolders"; + } + return null; + }, + }, + { + key: "ddd", + name: "Domain-Driven Design (DDD)", + description: "Models software around the business domain itself -- entities, value objects, aggregates, and bounded contexts -- built in close collaboration with domain experts using a shared \"ubiquitous language\". Best suited to genuinely complex business domains, not simple CRUD apps.", + link: "https://en.wikipedia.org/wiki/Domain-driven_design", + detect: function (ctx) { + const matched = ["entities", "aggregates", "value-objects", "valueobjects", "bounded-contexts"].filter(function (n) { return ctx.dirNames.has(n); }); + if (matched.length >= 2) return "directories present: " + matched.join(", "); + return null; + }, + }, + { + key: "serverless", + name: "Serverless", + description: "Application logic runs as short-lived, independently-deployed functions (Lambda, Cloud Functions, etc.) managed by a cloud provider, with no server process to keep running or scale manually. Trades infrastructure control for automatic scaling and pay-per-invocation billing.", + link: "https://en.wikipedia.org/wiki/Serverless_computing", + detect: function (ctx) { + if (ctx.rootFiles.has("serverless.yml")) return "serverless.yml present at the project root"; + if (ctx.rootFiles.has("serverless.yaml")) return "serverless.yaml present at the project root"; + if (ctx.dirNames.has("functions")) return "directory present: functions"; + if (ctx.dirNames.has("lambda")) return "directory present: lambda"; + const serverlessDeps = ["aws-lambda", "serverless", "@vercel/node", "netlify-lambda", "firebase-functions"].filter(function (d) { return ctx.dependencyNames.has(d); }); + if (serverlessDeps.length > 0) return "dependency: " + serverlessDeps.join(", "); + return null; + }, + }, + { + key: "plugin", + name: "Plugin Architecture", + description: "A stable core application loads independently-developed plugins at runtime to add functionality, without the core needing to know about any specific plugin in advance. Lets third parties (or other teams) extend the system without modifying its source.", + link: "https://en.wikipedia.org/wiki/Plug-in_(computing)", + detect: function (ctx) { + if (ctx.dirNames.has("plugins")) return "directory present: plugins"; + return null; + }, + }, + { + key: "pipeline", + name: "Pipeline Architecture", + description: "Data flows through a fixed sequence of independent processing stages, where each stage transforms its input and passes the result to the next. Common in build tools, compilers, and data-processing systems -- each stage can be developed, tested, and replaced in isolation.", + link: "https://en.wikipedia.org/wiki/Pipeline_(software)", + detect: function (ctx) { + const matched = ["pipeline", "pipelines", "stages"].filter(function (n) { return ctx.dirNames.has(n); }); + if (matched.length > 0) return "directory present: " + matched.join(", "); + return null; + }, + }, + { + key: "pubsub", + name: "Pub/Sub", + description: "Publishers send messages to named topics without knowing who (if anyone) is listening; subscribers register interest in topics without knowing who publishes to them. A specific, broker-mediated form of event-driven communication built around topics rather than direct events.", + link: "https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern", + detect: function (ctx) { + if (ctx.dirNames.has("pubsub")) return "directory present: pubsub"; + if (ctx.dirNames.has("topics") && ctx.dirNames.has("subscribers")) return "directories present: topics, subscribers"; + const pubsubDeps = ["@google-cloud/pubsub", "mqtt"].filter(function (d) { return ctx.dependencyNames.has(d); }); + if (pubsubDeps.length > 0) return "dependency: " + pubsubDeps.join(", "); + return null; + }, + }, + { + key: "actor-model", + name: "Actor Model", + description: "Independent \"actors\" each hold their own private state and communicate only by sending asynchronous messages to each other, never by sharing memory directly. Well suited to highly concurrent or distributed systems where isolating state per unit avoids shared-state bugs.", + link: "https://en.wikipedia.org/wiki/Actor_model", + detect: function (ctx) { + if (ctx.dirNames.has("actors")) return "directory present: actors"; + return null; + }, + }, + { + key: "component-based", + name: "Component-Based", + description: "The UI (or system) is built from independent, reusable components, each encapsulating its own markup/logic/state behind a well-defined interface. The default organizing principle in React, Vue, and Angular applications.", + link: "https://en.wikipedia.org/wiki/Component-based_software_engineering", + detect: function (ctx) { + if (ctx.dirNames.has("components")) return "directory present: components"; + return null; + }, + }, + { + key: "feature-based", + name: "Feature-Based", + description: "Code is grouped by feature or business capability rather than by technical role -- so everything related to one feature lives together, instead of being spread across separate controllers/, services/, models/ folders. Makes it easier to find and change everything one feature touches.", + link: "https://www.jimmybogard.com/vertical-slice-architecture/", + detect: function (ctx) { + const node = ctx.findTopNode("features") || ctx.findTopNode("modules"); + if (!node) return null; + if (isVerticalSliceNode(node)) return null; // reported as the more specific Vertical Slice pattern instead + return "directory present: " + node.name; + }, + }, + { + key: "vertical-slice", + name: "Vertical Slice", + description: "Each feature gets its own folder containing everything needed to fulfill it end-to-end -- UI, logic, and data access together -- rather than spreading one feature across shared technical layers. Popularized by Jimmy Bogard as a reaction against rigid, over-layered architectures.", + link: "https://www.jimmybogard.com/vertical-slice-architecture/", + detect: function (ctx) { + const node = ctx.findTopNode("features") || ctx.findTopNode("modules"); + if (!node || !isVerticalSliceNode(node)) return null; + return "directory \"" + node.name + "\" contains per-feature subfolders that each bundle their own controller/service/model-shaped files"; + }, + }, + { + key: "bff", + name: "BFF (Backend for Frontend)", + description: "Each frontend (web, iOS, Android, etc.) gets its own dedicated backend, tailored to exactly what that specific client needs, instead of every client sharing one general-purpose API. Popularized by Sam Newman to stop a single shared backend from becoming a bottleneck between frontend and backend teams.", + link: "https://samnewman.io/patterns/architectural/bff/", + detect: function (ctx) { + const dirMatch = Array.from(ctx.dirNames).find(function (n) { return /bff/i.test(n); }); + if (dirMatch) return "directory present: " + dirMatch; + const pkgMatch = ctx.workspacePackages.find(function (p) { return /bff/i.test(p.name); }); + if (pkgMatch) return "workspace package name: " + pkgMatch.name; + return null; + }, + }, + { + key: "api-gateway", + name: "API Gateway", + description: "A single entry point sits in front of multiple backend services, routing (and sometimes composing) client requests to the right one, so clients never call individual services directly. Common in microservice systems to hide internal service topology from consumers.", + link: "https://microservices.io/patterns/apigateway.html", + detect: function (ctx) { + if (ctx.dirNames.has("gateway")) return "directory present: gateway"; + if (ctx.dirNames.has("api-gateway")) return "directory present: api-gateway"; + const pkgMatch = ctx.workspacePackages.find(function (p) { return /gateway/i.test(p.name); }); + if (pkgMatch) return "workspace package name: " + pkgMatch.name; + return null; + }, + }, +]; + +/** + * Recursively find the first structure node matching `name` at any depth + * (e.g. "features" nested as src/features/, not just a rootDir-level + * directory) -- most real projects put this kind of folder under src/, + * not at the repo root, so a depth-1-only search would miss them. + * @param {object[]} structure + * @param {string} name + * @returns {object|null} + */ +function findStructureNodeByName(structure, name) { + for (let i = 0; i < (structure || []).length; i++) { + if (structure[i].name === name) return structure[i]; + if (structure[i].children) { + const found = findStructureNodeByName(structure[i].children, name); + if (found) return found; + } + } + return null; +} + +// Layer-shaped subdirectory names checked against a candidate +// "features"/"modules" child's OWN children -- distinguishes Vertical +// Slice from the more general Feature-Based signal (see +// ARCHITECTURE_PATTERN_DEFINITIONS above): Vertical Slice wins when a +// feature folder itself bundles its own mini-stack, not just related files. +const VERTICAL_SLICE_LAYER_DIR_NAMES = ["controllers", "services", "models", "routes", "components", "handlers"]; + +/** + * A features/modules node "is" Vertical Slice (rather than plain + * Feature-Based) when at least one of its own child directories itself + * contains 2+ of the layer-shaped subdirectory names above. + * @param {object} node - a getProjectStructure() node (already has .children). + * @returns {boolean} + */ +function isVerticalSliceNode(node) { + return (node.children || []).some(function (child) { + const childDirNames = (child.children || []).map(function (c) { return c.name; }); + const matched = VERTICAL_SLICE_LAYER_DIR_NAMES.filter(function (n) { return childDirNames.indexOf(n) !== -1; }); + return matched.length >= 2; + }); +} + +/** + * Detects which of a fixed reference catalog of common architectural + * patterns (ARCHITECTURE_PATTERN_DEFINITIONS) this project's own directory + * names, dependencies, and root-level files match. Every returned entry + * carries the same evidence-or-nothing discipline as + * getArchitectureSignals() -- a pattern only appears here when `detect()` + * found real, cited evidence, never a guess. This is the deeper companion + * to getArchitectureSignals()'s lighter "what kind of project is this" + * signals -- "what architecture pattern does this codebase actually use". + * @param {string} rootDir + * @param {object[]} [precomputedStructure] - a `getProjectStructure()` result + * to reuse instead of walking the filesystem again (getAllFacts() passes + * its own already-computed `structure` field here). Falls back to a fresh + * depth-4 walk when omitted, e.g. when this function is called standalone. + * Depth is capped at 4, not deeper: measured directly against this repo's + * own tree, depth 3->4 cost ~700ms extra (709ms -> 1413ms) but depth 4->5 + * cost ~6.4s more (7.8s) and depth 6 took ~19s -- a generated/versioned + * output directory sitting inside the scanned root (this repo's own + * docs-dashboard/site-versions/, which nests full copies of prior site + * generations) blows up combinatorially past depth 4. A real user's + * --out directory can just as easily live inside the directory being + * documented, so this cap protects every caller, not just this repo's + * own dogfooding case. + * @returns {{name: string, description: string, link: string, evidence: string}[]} + */ +function getArchitecturePatterns(rootDir, precomputedStructure) { + const pkg = readJsonSafe(path.join(rootDir, "package.json"), {}); + const workspacePackages = getWorkspacePackages(rootDir).map(function (p) { + return Object.assign({}, p, { dependencyNames: collectDependencyNames(path.join(rootDir, p.path, "package.json")) }); + }); + + const structureDeep = precomputedStructure || getProjectStructure(rootDir, { depth: 4 }); + const dirNames = collectDirectoryNamesFromStructure(structureDeep); + + let rootFileNames = []; + try { + rootFileNames = fs.readdirSync(rootDir, { withFileTypes: true }) + .filter(function (e) { return e.isFile(); }) + .map(function (e) { return e.name; }); + } catch (err) { + rootFileNames = []; + } + const rootFiles = new Set(rootFileNames); + + const dependencyNames = new Set(); + collectDependencyNames(path.join(rootDir, "package.json")).forEach(function (n) { dependencyNames.add(n); }); + workspacePackages.forEach(function (p) { p.dependencyNames.forEach(function (n) { dependencyNames.add(n); }); }); + + const ctx = { + dirNames: dirNames, + rootFiles: rootFiles, + pkg: pkg, + workspacePackages: workspacePackages, + dependencyNames: dependencyNames, + findTopNode: function (name) { return findStructureNodeByName(structureDeep, name); }, + }; + + return ARCHITECTURE_PATTERN_DEFINITIONS + .map(function (def) { + const evidence = def.detect(ctx); + if (evidence == null) return null; + return { name: def.name, description: def.description, link: def.link, evidence: evidence }; + }) + .filter(Boolean); +} + /** * Test-tooling facts: no framework, hand-rolled runner, no HTTP API. * @param {string} rootDir @@ -481,14 +881,22 @@ function getTestInfo(rootDir) { */ function getAllFacts(rootDir) { const dir = rootDir || process.cwd(); + // Walked once at depth 4 (one level deeper than getProjectStructure()'s + // own default of 3) and reused for both the `structure` field and + // getArchitecturePatterns() below -- avoids a second, redundant + // filesystem walk purely for pattern detection (see + // getArchitecturePatterns()'s own doc comment for why depth is capped + // at 4, not deeper). + const structure = getProjectStructure(dir, { depth: 4 }); return { node: getNodeVersions(dir), packageManager: getPackageManager(dir), globalDependencies: getGlobalDependencies(dir), - structure: getProjectStructure(dir), + structure: structure, workspacePackages: getWorkspacePackages(dir), frameworkSignals: getFrameworkSignals(dir), architectureSignals: getArchitectureSignals(dir), + architecturePatterns: getArchitecturePatterns(dir, structure), test: getTestInfo(dir), }; } @@ -501,6 +909,7 @@ module.exports = { getWorkspacePackages, getFrameworkSignals, getArchitectureSignals, + getArchitecturePatterns, getTestInfo, getAllFacts, }; diff --git a/lib/renderer.js b/lib/renderer.js index 4b034b0..9b2a0e4 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -7,29 +7,30 @@ const path = require("path"); // --------------------------------------------------------------------------- const CSS_STRUCTURE = ` -@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"); -:root{--topnav-h:64px;--sidebar-w:224px;--toc-w:220px;--accent:#5B4FE8;--accent-hover:#4A3FD1;--accent-bg:rgba(91,79,232,.12);--text:#0D0D0D;--text2:#3A3A3A;--text3:#8A8A8A;--border:rgba(13,13,13,.10);--bg:#F5F4F0;--surface:#ffffff;--sidebar-bg:#0E0E10;--sidebar-section:#8C8981;--sidebar-text:#B7B4AC;--sidebar-text-hover:#F5F4F0;--sidebar-hover-bg:rgba(245,244,240,.06);--topnav-bg:#ffffff;--code-bg:#0E0E10;--code-text:#B7B4AC;--lime:#C6FF3D;--coral:#FF4B2E;--gold:#F5C518;--black:#0D0D0D;--black-soft:#161616;--offwhite:#F5F4F0;--offwhite-soft:#FAF9F5;--gray-900:#1A1A1A;--gray-700:#3A3A3A;--gray-500:#8A8A8A;--gray-300:#C7C5BE;--gray-200:#DEDCD4;--gray-100:#EAE8E1;--purple:#5B4FE8;--purple-hover:#4A3FD1;--purple-press:#3E34B8;--lime-hover:#B8F02A;--lime-press:#A6DC1E;--coral-hover:#E83E22;--coral-press:#D03418;--border-on-dark:rgba(245,244,240,0.12);--border-on-light:rgba(13,13,13,0.10);--text-on-lime:#0D0D0D;--font-display:"Space Grotesk","Inter Tight",-apple-system,sans-serif;--font-body:"Space Grotesk","Inter Tight",-apple-system,sans-serif;--font-mono:"JetBrains Mono","Fira Code",ui-monospace,monospace;--tracking-display:-0.02em;--text-mono-label:500 11px/1.2 var(--font-mono);--text-mono-badge:600 10px/1 var(--font-mono);--tracking-mono-label:0.08em;--text-button:500 15px/1 var(--font-body);--text-body-sm:400 14px/1.5 var(--font-body);--radius-sm:10px;--radius-md:16px;--radius-lg:24px;--radius-xl:28px;--radius-pill:999px;--shadow-card:0 2px 8px rgba(13,13,13,0.05), 0 8px 24px rgba(13,13,13,0.06);--shadow-inset-dark:inset 0 1px 0 rgba(255,255,255,0.04)} +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"); +:root{--topnav-h:56px;--sidebar-w:224px;--toc-w:220px;--accent:#7382FF;--accent-hover:#4D5BFF;--accent-press:#3442F0;--accent-bg:rgba(115,130,255,.13);--text:#F4F5F7;--text2:#C2C7D0;--text3:#8A909A;--text4:#717680;--border:#2B2F36;--bg:#0F1115;--surface:#171A20;--surface-hover:#1D2128;--sidebar-bg:#171A20;--sidebar-section:#535862;--sidebar-text:#8A909A;--sidebar-text-hover:#F4F5F7;--sidebar-hover-bg:#1D2128;--topnav-bg:#171A20;--code-bg:#0F1115;--code-text:#8A909A;--success:#34D399;--warning:#FBBF24;--danger:#F87171;--info:#38BDF8;--success-bg:#064E3B33;--warning-bg:#78350F33;--danger-bg:#7F1D1D33;--info-bg:#0C4A6E33;--black:#0F1115;--black-soft:#12151B;--gray-900:#171A20;--gray-700:#3A4048;--gray-500:#717680;--gray-300:#8A909A;--gray-200:#2B2F36;--gray-100:#1D2128;--border-on-dark:rgba(244,245,247,0.12);--border-on-light:rgba(15,17,21,0.10);--text-inverse:#0F1115;--font-display:"Geist","Inter",-apple-system,sans-serif;--font-body:"IBM Plex Sans","Geist","Inter",-apple-system,sans-serif;--font-mono:"JetBrains Mono","Fira Code",ui-monospace,monospace;--tracking-display:-0.02em;--text-mono-label:500 11px/1.2 var(--font-mono);--text-mono-badge:600 10px/1 var(--font-mono);--tracking-mono-label:0.08em;--text-button:500 13px/1 var(--font-body);--text-body-sm:400 14px/1.5 var(--font-body);--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-xl:12px;--radius-pill:999px;--ease:cubic-bezier(0.215,0.610,0.355,1.000);--dur-fast:120ms;--dur-normal:180ms;--dur-complex:240ms;--shadow-card:0 2px 8px rgba(0,0,0,0.24), 0 8px 24px rgba(0,0,0,0.28);--shadow-inset-dark:inset 0 1px 0 rgba(255,255,255,0.04)} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +.icon{flex-shrink:0;display:inline-block;vertical-align:middle} html{scroll-behavior:smooth} -body{font-family:var(--font-body);font-size:15px;line-height:1.6;color:var(--text);background:var(--bg)} +body{font-family:var(--font-body);font-size:14px;line-height:1.5;color:var(--text);background:var(--bg);font-variant-numeric:tabular-nums} a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline} code,pre{font-family:var(--font-mono)} .topnav{position:sticky;top:0;z-index:200;background:var(--topnav-bg);border-bottom:1px solid var(--border);height:var(--topnav-h)} -.topnav-inner{display:flex;align-items:center;gap:24px;height:100%;padding:0 28px;justify-content:space-between} -.topnav-logo{font-size:15px;font-weight:700;color:var(--text);text-decoration:none;white-space:nowrap;flex-shrink:0} +.topnav-inner{display:flex;align-items:center;gap:16px;height:100%;padding:0 24px;justify-content:flex-start} +.topnav-logo{font-size:13px;font-weight:600;color:var(--text);text-decoration:none;white-space:nowrap;flex-shrink:0;font-family:var(--font-display)} .topnav-logo:hover{color:var(--accent)} -.topnav-version{font-size:11px;font-weight:400;color:var(--text3);font-family:monospace;margin-left:4px} -.topnav-search{flex:none;max-width:360px;margin:0;position:relative} -.search-box{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:999px;padding:7px 32px 7px 32px;color:var(--text);font-size:13px;outline:none} -.search-box:focus{border-color:var(--accent);background:var(--surface)} -.search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;opacity:.5;pointer-events:none} -.search-kbd{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:var(--border);border-radius:3px;padding:1px 5px;font-size:11px;color:var(--text3);font-family:monospace;pointer-events:none} +.topnav-version{font-size:11px;font-weight:400;color:var(--text3);font-family:var(--font-mono);margin-left:4px} +.topnav-search{flex:1;max-width:280px;margin:0;position:relative} +.search-box{width:100%;background:var(--surface-hover);border:1px solid var(--border);border-radius:var(--radius-md);padding:7px 32px 7px 32px;color:var(--text2);font-size:12px;font-family:var(--font-body);outline:none;transition:border-color var(--dur-fast) var(--ease)} +.search-box:focus{border-color:var(--accent);background:var(--surface-hover)} +.search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:13px;height:13px;color:var(--text3);opacity:1;pointer-events:none} +.search-kbd{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:var(--surface);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-size:9px;color:var(--text3);font-family:var(--font-mono);pointer-events:none} .search-results{display:none;position:absolute;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:8px;max-height:320px;overflow-y:auto;z-index:300;margin-top:4px;box-shadow:0 8px 32px rgba(0,0,0,.12)} .search-results.visible{display:block} .search-result-item{display:block;padding:9px 14px;cursor:pointer;border-bottom:1px solid var(--border);text-decoration:none} .search-result-item:hover{background:var(--bg)} -.sr-name{font-size:13px;font-weight:600;color:var(--text);font-family:monospace} +.sr-name{font-size:13px;font-weight:600;color:var(--text);font-family:var(--font-mono)} .sr-kind{font-size:10px;color:var(--text3);margin-left:6px;text-transform:uppercase;letter-spacing:.05em} .sr-module{font-size:11px;color:var(--text3);display:block;margin-top:2px} .sr-preview{font-size:11px;color:var(--text2);font-style:italic;display:block;margin-top:1px} @@ -37,16 +38,16 @@ code,pre{font-family:var(--font-mono)} .layout{display:grid;grid-template-columns:var(--sidebar-w) 1fr;min-height:calc(100vh - var(--topnav-h))} .layout-toc{grid-template-columns:var(--sidebar-w) 1fr var(--toc-w)} .sidebar{grid-column:1;background:var(--sidebar-bg);border-right:1px solid var(--border);position:sticky;top:var(--topnav-h);height:calc(100vh - var(--topnav-h));overflow-y:auto} -.sidebar-inner{padding:16px 0 32px} +.sidebar-inner{padding:0 0 32px} .sidebar-inner[role="tree"]{} -.sidebar-section-title{padding:16px 16px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--sidebar-section);position:sticky;top:0;z-index:2;background:var(--sidebar-bg);} +.sidebar-section-title{padding:16px 16px 4px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.14em;color:var(--sidebar-section);font-family:var(--font-mono);position:sticky;top:0;z-index:2;background:var(--sidebar-bg);} .sidebar-link{display:block;padding:6px 16px;font-size:13px;color:var(--sidebar-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .1s,background .1s;border-radius:6px} .sidebar-link:hover{color:var(--sidebar-text-hover);background:var(--sidebar-hover-bg);text-decoration:none} .sidebar-link.active{color:#fff;font-weight:600;background:var(--accent);border-left:2px solid var(--accent);padding-left:14px;border-radius:6px} .sidebar-dir-toggle{display:flex;align-items:center;gap:5px;cursor:pointer;list-style:none;padding:6px 16px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--sidebar-section);user-select:none;position:sticky;z-index:1;background:var(--sidebar-bg);} .sidebar-dir-toggle::-webkit-details-marker{display:none} -.sidebar-dir-toggle::before{content:'▶';font-size:8px;transition:transform .15s;flex-shrink:0} -details[open] .sidebar-dir-toggle::before{transform:rotate(90deg)} +.sidebar-dir-toggle::before{content:'';width:6px;height:6px;border-right:1.75px solid var(--sidebar-section);border-bottom:1.75px solid var(--sidebar-section);transform:rotate(-45deg);transition:transform var(--dur-fast) var(--ease);flex-shrink:0;display:inline-block} +details[open] .sidebar-dir-toggle::before{transform:rotate(45deg)} .sidebar-link-indent{padding-left:28px} .sidebar-link-indent.active{padding-left:26px} .sidebar-dir-toggle,.sidebar-link{padding-left:calc(16px + (var(--depth,0) * 14px))} @@ -64,93 +65,93 @@ details[open] .sidebar-dir-toggle::before{transform:rotate(90deg)} .sym-row{display:flex;align-items:center;gap:5px;padding:2px 10px 2px 38px;min-width:0} .sym-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;display:inline-block} .sym-kind-abbr{font-size:9px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--sidebar-section);width:16px;flex-shrink:0;display:inline-block} -.sym-fn{background:var(--lime)} +.sym-fn{background:var(--success)} .sym-cls{background:var(--accent)} -.sym-iface{background:var(--coral)} -.sym-enum{background:var(--coral)} +.sym-iface{background:var(--info)} +.sym-enum{background:var(--danger)} .sym-type{background:var(--sidebar-text)} -.sym-var{background:var(--gold)} +.sym-var{background:var(--warning)} .sym-link{font-size:12px;color:var(--sidebar-text);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0} .sym-link:hover{color:var(--sidebar-text-hover);text-decoration:none} .sym-link.active{color:#fff;font-weight:600} .main{min-width:0;overflow-x:hidden} -.page-header{padding:32px 48px 24px;border-bottom:1px solid var(--border)} -.page-title{font-size:26px;font-weight:700;color:var(--text);margin-bottom:4px;letter-spacing:-.02em} -.page-subtitle{color:var(--text3);font-size:12px;margin-bottom:0;font-family:monospace} +.page-header{padding:26px 32px 24px;border-bottom:1px solid var(--border)} +.page-title{font-size:22px;font-weight:600;color:var(--text);margin-bottom:4px;letter-spacing:-.02em;font-family:var(--font-display)} +.page-subtitle{color:var(--text3);font-size:12px;margin-bottom:0;font-family:var(--font-mono)} .module-desc{color:var(--text2);font-size:14px;line-height:1.7;margin-top:10px;max-width:600px} .breadcrumb{font-size:13px;color:var(--text3);margin-bottom:12px} .breadcrumb a{color:var(--accent)} .section{margin-bottom:0} -.section-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);padding:20px 32px 10px;display:flex;align-items:center;gap:8px;border-top:1px solid var(--border);background:#f9fbfd} +.section-title{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--sidebar-section);font-family:var(--font-mono);padding:20px 32px 10px;display:flex;align-items:center;gap:8px;border-top:1px solid var(--border);background:var(--surface)} .section:first-child .section-title{border-top:none} .index-content .section-title{border-top:none} -.section-count{font-size:10px;font-weight:400;color:#b0bec8;font-family:monospace} +.section-count{font-size:10px;font-weight:400;color:var(--text3);font-family:var(--font-mono)} .card{display:grid;grid-template-columns:1fr 380px;background:var(--surface);border-bottom:1px solid var(--border);scroll-margin-top:calc(var(--topnav-h) + 8px)} -.card-prose{padding:24px 32px;border-right:1px solid var(--border);min-width:0} -.card-code{background:var(--black-soft);padding:22px 20px;min-width:0;overflow:hidden} +.card-prose{padding:26px 32px;border-right:1px solid var(--border);min-width:0} +.card-code{background:var(--black-soft);padding:24px 22px;min-width:0;overflow:hidden} .card-header{display:flex;align-items:flex-start;gap:8px;margin-bottom:4px} -.card-name{font-size:14px;font-weight:700;color:var(--text);font-family:monospace;flex:1;min-width:0;word-break:break-all} -.card-sig{font-size:12px;color:var(--text2);font-family:monospace;margin-top:4px;word-break:break-word;line-height:1.5} +.card-name{font-size:14px;font-weight:700;color:var(--text);font-family:var(--font-mono);flex:1;min-width:0;word-break:break-all} +.card-sig{font-size:12px;color:var(--text2);font-family:var(--font-mono);margin-top:4px;word-break:break-word;line-height:1.5} .card-desc{font-size:13px;color:var(--text2);margin-top:10px;line-height:1.6;max-width:520px} -.card-anchor{color:#b0bec8;opacity:0;font-size:12px;margin-left:3px;transition:opacity .15s;text-decoration:none;flex-shrink:0} +.card-anchor{color:var(--text3);opacity:0;font-size:12px;margin-left:3px;transition:opacity var(--dur-fast) var(--ease);text-decoration:none;flex-shrink:0} .card:hover .card-anchor{opacity:.6} .card-anchor:hover{opacity:1;text-decoration:none} -.code-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#4a6f8a;margin-bottom:8px} -.card-code pre{margin:0;font-size:12px;line-height:1.65;color:var(--code-text);overflow-x:auto;white-space:pre-wrap;word-break:break-word;tab-size:2} -.copy-btn{float:right;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.13);border-radius:4px;padding:3px 8px;font-size:10px;color:#7a95b0;cursor:pointer;transition:all .15s;margin:-2px 0 6px 6px} -.copy-btn:hover{background:rgba(255,255,255,.13);color:#c8daea} -.copy-btn.copied{background:rgba(74,222,128,.15);border-color:rgba(74,222,128,.3);color:#4ade80} -.badge{display:inline-block;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:.02em;margin-right:3px;margin-top:4px;vertical-align:middle} -.badge-exported{background:#dcfce7;color:#166534} -.badge-async{background:#eff6ff;color:#1e40af} -.badge-abstract{background:#fce7f3;color:#9d174d} -.badge-static{background:#fefce8;color:#854d0e} -.badge-readonly{background:#f5f3ff;color:#5b21b6} -.badge-generator{background:#ecfdf5;color:#065f46} -.badge-deprecated{background:#fef2f2;color:#991b1b} -.badge-since{background:#f0fdf4;color:#166534} -.badge-optional{background:#f5f3ff;color:#5b21b6} -.badge-const{background:#ede9fe;color:#4c1d95} -.badge-var{background:#fff7ed;color:#9a3412} -.badge-private{background:#f8fafc;color:#64748b;border:1px solid #e2e8f0} -.badge-protected{background:#fefce8;color:#92400e} -.badge-public{background:#f0fdf4;color:#166534} -.deprecated-notice{background:#fef2f2;color:#991b1b;border-radius:5px;padding:8px 14px;font-size:13px;margin-top:10px;border:1px solid #fecaca} +.code-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--info);font-family:var(--font-mono);margin-bottom:8px} +.card-code pre{margin:0;font-size:12px;line-height:1.65;color:var(--code-text);overflow-x:auto;white-space:pre-wrap;word-break:break-word;tab-size:2;font-family:var(--font-mono)} +.copy-btn{float:right;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:var(--radius-sm);padding:3px 8px;font-size:10px;color:var(--text3);cursor:pointer;transition:all var(--dur-fast) var(--ease);margin:-2px 0 6px 6px;font-family:var(--font-mono)} +.copy-btn:hover{background:rgba(255,255,255,.12);color:var(--text2)} +.copy-btn.copied{background:var(--success-bg);border-color:var(--success);color:var(--success)} +.badge{display:inline-block;padding:2px 7px;border-radius:var(--radius-pill);font-size:10px;font-weight:600;letter-spacing:.02em;margin-right:3px;margin-top:4px;vertical-align:middle;font-family:var(--font-mono);text-transform:uppercase} +.badge-exported{background:var(--success-bg);color:var(--success)} +.badge-async{background:var(--info-bg);color:var(--info)} +.badge-abstract{background:var(--accent-bg);color:var(--accent)} +.badge-static{background:var(--warning-bg);color:var(--warning)} +.badge-readonly{background:var(--accent-bg);color:var(--accent)} +.badge-generator{background:var(--success-bg);color:var(--success)} +.badge-deprecated{background:var(--danger-bg);color:var(--danger)} +.badge-since{background:var(--success-bg);color:var(--success)} +.badge-optional{background:var(--accent-bg);color:var(--accent)} +.badge-const{background:var(--accent-bg);color:var(--accent)} +.badge-var{background:var(--warning-bg);color:var(--warning)} +.badge-private{background:var(--surface-hover);color:var(--text3);border:1px solid var(--border)} +.badge-protected{background:var(--warning-bg);color:var(--warning)} +.badge-public{background:var(--success-bg);color:var(--success)} +.deprecated-notice{display:flex;align-items:center;gap:6px;background:var(--danger-bg);color:var(--danger);border-radius:var(--radius-md);padding:8px 14px;font-size:13px;margin-top:10px;border:1px solid var(--danger)} .since-label{font-size:11px;color:var(--text3);margin-top:4px} .params-table{width:100%;border-collapse:collapse;margin-top:14px;font-size:13px} -.params-table th{text-align:left;padding:6px 10px;background:#f8fafc;color:var(--text3);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border)} -.params-table td{padding:8px 10px;border-bottom:1px solid #f1f5f9;color:var(--text);vertical-align:top;line-height:1.5} -.params-table td code{background:#f1f5f9;padding:1px 5px;border-radius:3px;font-size:12px;color:var(--text2)} +.params-table th{text-align:left;padding:7px 12px;background:var(--surface-hover);color:var(--text3);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);font-family:var(--font-mono)} +.params-table td{padding:9px 12px;border-bottom:1px solid var(--border);color:var(--text);vertical-align:top;line-height:1.5} +.params-table td code{background:var(--surface-hover);padding:1px 5px;border-radius:3px;font-size:12px;color:var(--text2)} .params-table td:first-child code{color:var(--text);font-weight:600} -.returns{margin-top:12px;font-size:13px;color:var(--text2);padding-top:10px;border-top:1px solid #f1f5f9} -.returns code{background:#f1f5f9;padding:1px 6px;border-radius:3px;font-family:monospace;font-size:12px} +.returns{margin-top:12px;font-size:13px;color:var(--text2);padding-top:10px;border-top:1px solid var(--border)} +.returns code{background:var(--surface-hover);padding:1px 6px;border-radius:3px;font-family:var(--font-mono);font-size:12px} .throws-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:13px} -.throws-table th{text-align:left;padding:6px 10px;background:#fef2f2;color:#991b1b;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #fecaca} -.throws-table td{padding:7px 10px;border-bottom:1px solid #fff5f5;color:var(--text);vertical-align:top} -.throws-table td code{font-size:12px;color:#991b1b} -.collapse-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none;list-style:none;margin-top:16px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--text3);padding:0} +.throws-table th{text-align:left;padding:7px 12px;background:var(--danger-bg);color:var(--danger);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--danger);font-family:var(--font-mono)} +.throws-table td{padding:8px 12px;border-bottom:1px solid var(--border);color:var(--text);vertical-align:top} +.throws-table td code{font-size:12px;color:var(--danger)} +.collapse-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none;list-style:none;margin-top:16px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--text3);padding:0;font-family:var(--font-mono)} .collapse-toggle::-webkit-details-marker{display:none} -.collapse-toggle::before{content:'▶';font-size:8px;transition:transform .15s;color:#b0bec8} -details[open] .collapse-toggle::before{transform:rotate(90deg)} +.collapse-toggle::before{content:'';width:6px;height:6px;border-right:1.75px solid var(--text3);border-bottom:1.75px solid var(--text3);transform:rotate(-45deg);transition:transform var(--dur-fast) var(--ease);display:inline-block} +details[open] .collapse-toggle::before{transform:rotate(45deg)} .collapse-body{margin-top:6px} -.method-row{margin-top:8px;padding:10px 0;border-top:1px solid #f1f5f9} -.method-sig{font-family:monospace;font-size:13px;color:var(--text)} +.method-row{margin-top:8px;padding:10px 0;border-top:1px solid var(--border)} +.method-sig{font-family:var(--font-mono);font-size:13px;color:var(--text)} .method-desc{font-size:12px;color:var(--text3);margin-top:4px} -.source-link{font-size:11px;color:var(--text3);font-family:monospace;text-decoration:none;flex-shrink:0} +.source-link{font-size:11px;color:var(--text3);font-family:var(--font-mono);text-decoration:none;flex-shrink:0} .source-link:hover{color:var(--accent);text-decoration:none} .link-ref{color:var(--accent);text-decoration:none} .link-ref:hover{text-decoration:underline} -.anchor-link{color:#b0bec8;opacity:0;font-size:13px;margin-left:6px;transition:opacity .15s} +.anchor-link{color:var(--text3);opacity:0;font-size:13px;margin-left:6px;transition:opacity var(--dur-fast) var(--ease)} .card:hover .anchor-link{opacity:.6} .anchor-link:hover{opacity:1;text-decoration:none} .empty{color:var(--text3);font-size:13px;font-style:italic;padding:24px 32px} -.index-content{padding:32px 48px 80px} +.index-content{padding:36px 52px 80px} .module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin-top:8px} .module-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:18px 20px;display:block;transition:border-color .15s,box-shadow .15s} -.module-card:hover{border-color:var(--accent);box-shadow:0 2px 12px rgba(98,91,246,.1);text-decoration:none} +.module-card:hover{border-color:var(--accent);box-shadow:0 2px 12px rgba(115,130,255,.18);text-decoration:none} .module-card-path{font-size:11px;font-weight:500;color:var(--text2);margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.01em} -.module-card-path-sep{color:#c8d3dc;margin:0 3px;font-weight:400} -.module-card-name{font-size:14px;font-weight:700;color:var(--text);font-family:monospace;margin-bottom:4px;display:flex;align-items:center;gap:6px;flex-wrap:wrap} +.module-card-path-sep{color:var(--text3);margin:0 3px;font-weight:400} +.module-card-name{font-size:14px;font-weight:700;color:var(--text);font-family:var(--font-mono);margin-bottom:4px;display:flex;align-items:center;gap:6px;flex-wrap:wrap} .module-card-stats{font-size:12px;color:var(--text3)} .module-card-desc{font-size:12px;color:var(--text3);margin-top:6px;line-height:1.4} .tok-kw{color:#79b8ff;font-weight:500} @@ -176,40 +177,48 @@ details[open] .collapse-toggle::before{transform:rotate(90deg)} .qsub-title{font-size:13px;font-weight:700;color:var(--text2);margin:22px 0 8px} .qtable-wrap{overflow-x:auto} .qtable{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:8px} -.qtable th{text-align:left;padding:6px 10px;background:#f8fafc;color:var(--text3);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border)} -.qtable td{padding:7px 10px;border-bottom:1px solid #f1f5f9;color:var(--text);vertical-align:top} -.qtable tbody tr:nth-child(even){background:#f9fbfd} -.qtable td code{background:#f1f5f9;padding:1px 5px;border-radius:3px;font-size:12px} +.qtable th{text-align:left;padding:6px 10px;background:var(--surface-hover);color:var(--text3);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);font-family:var(--font-mono)} +.qtable td{padding:7px 10px;border-bottom:1px solid var(--border);color:var(--text);vertical-align:top} +.qtable tbody tr:nth-child(even){background:rgba(255,255,255,.02)} +.qtable td code{background:var(--surface-hover);padding:1px 5px;border-radius:3px;font-size:12px} .qempty{color:var(--text3);font-size:13px;font-style:italic;padding:4px 0 16px} .qcard-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin:16px 0 24px} .qcard{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px 18px;display:flex;flex-direction:column;gap:10px} -.qcard-title{font-size:13px;font-weight:700;color:var(--text)} -.qcard-statline{font-size:12px;color:var(--text3);margin-top:2px} +.qcard-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.qcard-statline{font-size:12px;color:var(--text3);margin-top:2px;padding:0 16px} +.arch-tree>details{margin-bottom:2px} +.arch-tree .collapse-toggle{margin-top:10px;flex-wrap:wrap;row-gap:4px} +.arch-tree .collapse-body{margin-top:4px;margin-left:2px;padding-left:16px;border-left:1px solid var(--border)} +.arch-node-name{font-weight:700} +.arch-badges{display:inline-flex;flex-wrap:wrap;align-items:center;gap:6px;margin-left:10px} +.arch-badge{display:inline-flex;align-items:center;font-size:10.5px;font-weight:600;font-family:var(--font-mono);padding:2px 8px;border-radius:var(--radius-pill);border:1px solid transparent;line-height:1.5;white-space:nowrap} +@media(max-width:720px){.arch-badges{margin-left:22px;width:100%;margin-top:2px}} .qcard-preview{display:flex;flex-direction:column} -.qcard-row{display:flex;justify-content:space-between;gap:8px;padding:5px 0;font-size:12.5px;border-bottom:1px solid #f1f5f9;color:var(--text2)} +.qcard-row{display:flex;justify-content:space-between;gap:8px;padding:5px 0;font-size:12.5px;border-bottom:1px solid var(--border);color:var(--text2);font-family:var(--font-mono)} .qcard-row:last-child{border-bottom:none} .qcard-row a{color:var(--accent);text-decoration:none} .qcard-row a:hover{text-decoration:underline} -.qcard-more{margin-top:2px;font-size:12px;font-weight:600;color:var(--accent);text-decoration:none;display:inline-flex;align-items:center;gap:4px} +.qcard-more{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;font-family:var(--font-mono);text-decoration:none;display:inline-flex;align-items:center;gap:4px;transition:opacity var(--dur-fast) var(--ease)} +.qcard-more:hover{opacity:.7} .focus-btn{display:flex;justify-content:center;font:var(--text-mono-badge);letter-spacing:var(--tracking-mono-label);text-transform:uppercase;border-radius:var(--radius-pill);padding:10px 16px;margin-top:6px;text-decoration:none;transition:filter .15s ease} .focus-btn:hover{filter:brightness(0.92);text-decoration:none} .focus-btn:active{filter:brightness(0.85)} -.qcard-more:hover{text-decoration:underline} .qback{display:inline-block;font-size:13px;color:var(--accent);text-decoration:none;margin-bottom:16px} .qback:hover{text-decoration:underline} .qstrip{margin:0 32px 16px;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:8px;display:flex;flex-wrap:wrap} .qchip{display:flex;flex-direction:column;padding:4px 14px;border-right:1px solid var(--border);min-width:84px} .qchip:last-child{border-right:none} .qchip-label{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text3)} -.qchip-value{font-size:14px;font-weight:700;color:var(--text);font-family:monospace;margin-top:1px} +.qchip-value{font-size:14px;font-weight:700;color:var(--text);font-family:var(--font-mono);margin-top:1px} .qchip-value a{text-decoration:none} .qchip-value a:hover{text-decoration:underline} .qchip-empty{padding:6px 14px;font-size:12.5px;color:var(--text3);font-style:italic} .qcard-row a:focus-visible,.qcard-more:focus-visible,.qback:focus-visible,.qchip-value a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px} @media(max-width:720px){.qstrip{margin-left:24px;margin-right:24px}.qchip{border-right:none;border-bottom:1px solid var(--border);width:50%}.qchip:nth-child(odd){border-right:1px solid var(--border)}} -.qhero{background:var(--code-bg);border-radius:10px;padding:24px 28px;display:flex;align-items:center;gap:28px;flex-wrap:wrap;margin:8px 0 20px} +.qhero{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:24px 28px;display:flex;align-items:center;gap:28px;flex-wrap:wrap;margin:8px 0 20px} .qhero-gauge{position:relative;width:168px;height:168px;flex:none;border-radius:50%;display:flex;align-items:center;justify-content:center} -.qhero-gauge-inner{position:absolute;inset:13px;border-radius:50%;background:var(--code-bg);display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center} +.qhero-gauge-svg{position:absolute;inset:0;transform:rotate(0deg)} +.qhero-gauge-inner{position:absolute;inset:13px;border-radius:50%;background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center} .qhero-grade{font-size:44px;font-weight:800;line-height:1} .qhero-score{font-size:21px;font-weight:700;color:#fff;margin-top:3px} .qhero-score-label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--code-text);margin-top:3px} @@ -218,7 +227,7 @@ details[open] .collapse-toggle::before{transform:rotate(90deg)} .qhero-metric{display:flex;align-items:center;gap:8px;font-size:11.5px} .qhero-metric-dot{width:7px;height:7px;border-radius:50%;flex:none} .qhero-metric-label{text-transform:uppercase;letter-spacing:.05em;font-size:10.5px;color:var(--code-text);flex:1} -.qhero-metric-value{font-weight:700;color:#fff;font-family:monospace;font-size:12.5px} +.qhero-metric-value{font-weight:700;color:#fff;font-family:var(--font-mono);font-size:12.5px} .qhero-trend{flex:1;min-width:200px} .qhero-trend-badge{display:inline-block;color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:4px 10px;border-radius:99px;margin-bottom:8px} .qhero-sparkline{display:block} @@ -226,14 +235,13 @@ details[open] .collapse-toggle::before{transform:rotate(90deg)} .qhero-trend-empty{font-size:12.5px;line-height:1.5;color:var(--code-text);max-width:340px} .qhero-trend-empty code{color:#fff;font-size:11.5px} @media(max-width:720px){.qhero{flex-direction:column;align-items:stretch}.qhero-divider{display:none}} -.qcard2{background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;display:flex;flex-direction:column} -.qcard2-bar{height:4px} -.qcard2-body{padding:16px 18px;display:flex;flex-direction:column;gap:10px;flex:1} -.qcard2-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px} -.qcard2-big{text-align:right;flex:none;padding-left:8px} -.qcard2-bigvalue{font-size:22px;font-weight:700;font-family:monospace;line-height:1} -.qcard2-bigcaption{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);white-space:nowrap;margin-top:3px} -.tag-chip{display:inline-block;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:5px 11px;border-radius:99px;margin-bottom:7px;border:1.5px solid #fff} +.qcard2{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);overflow:hidden;display:flex;flex-direction:column} +.qcard2-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border)} +.qcard2-head-left{display:flex;align-items:center;gap:8px;min-width:0} +.qcard2-body{padding:6px 16px 14px;display:flex;flex-direction:column;gap:2px;flex:1} +.qcard2-bigvalue{font-size:20px;font-weight:600;font-family:var(--font-display);line-height:1;font-variant-numeric:tabular-nums;flex-shrink:0} +.qcard2-foot{padding:10px 16px;border-top:1px solid var(--border)} +.tag-chip{display:inline-flex;align-items:center;gap:5px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;font-family:var(--font-mono)} .badge-pill{display:flex;align-items:stretch;border-radius:var(--radius-pill);overflow:hidden;flex:none;border:1.5px solid #fff} .badge-label{font:var(--text-mono-badge);letter-spacing:var(--tracking-mono-label);text-transform:uppercase;background:var(--black);color:var(--gray-300);padding:7px 13px;display:flex;align-items:center} .badge-value{font:700 12px var(--font-mono);padding:7px 13px;display:flex;align-items:center;justify-content:flex-end;flex:1} @@ -244,8 +252,8 @@ details[open] .collapse-toggle::before{transform:rotate(90deg)} .qcard-expand{margin-top:2px} .qcard-expand-toggle{cursor:pointer;list-style:none;font-size:12px;font-weight:600;color:var(--accent);padding:2px 0;user-select:none;display:flex;align-items:center;gap:5px} .qcard-expand-toggle::-webkit-details-marker{display:none} -.qcard-expand-toggle::before{content:'\u25B8';font-size:9px;transition:transform .15s} -details[open]>.qcard-expand-toggle::before{transform:rotate(90deg)} +.qcard-expand-toggle::before{content:'';width:5px;height:5px;border-right:1.75px solid var(--accent);border-bottom:1.75px solid var(--accent);transform:rotate(-45deg);transition:transform var(--dur-fast) var(--ease);display:inline-block} +details[open]>.qcard-expand-toggle::before{transform:rotate(45deg)} .qcard-expand-body{padding-top:4px} .qcard-expand-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px} .qhero-file{margin:0 32px 24px} @@ -265,34 +273,35 @@ details[open]>.qcard-expand-toggle::before{transform:rotate(90deg)} .qfn-metric-dots{display:inline-flex;align-items:center;gap:4px} .qfn-metric-dot{width:6px;height:6px} .qfn-smells{font-size:11px;color:var(--text3);font-style:italic} -.todo-badge{display:inline-block;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;background:#FFF3CF;color:#8A6400;padding:2px 7px;border-radius:99px;margin-right:6px;vertical-align:middle} +.todo-badge{display:inline-block;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;background:var(--warning-bg);color:var(--warning);padding:2px 7px;border-radius:99px;margin-right:6px;vertical-align:middle;font-family:var(--font-mono)} .todo-text{font-style:italic;color:var(--text3)} -.sidebar-brand{padding:2px 10px 4px;display:flex;align-items:baseline;gap:8px} -.sidebar-logo{font-size:19px;font-weight:700;color:var(--sidebar-text-hover);letter-spacing:-.01em} -.sidebar-version{padding:0 10px 18px;font-size:10px;font-weight:700;letter-spacing:.05em;color:var(--text3)} -.navbtn{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:8px;border:none;cursor:pointer;background:transparent;color:var(--sidebar-text);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;width:100%;text-align:left;box-sizing:border-box;text-decoration:none;margin-bottom:2px} +.sidebar-brand{padding:0 16px;display:flex;align-items:center;gap:9px;height:56px;border-bottom:1px solid var(--border)} +.sidebar-mark{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:var(--radius-md);background:var(--accent);color:var(--bg);flex-shrink:0} +.sidebar-logo{font-size:13px;font-weight:600;color:var(--sidebar-text-hover);letter-spacing:-.01em;font-family:var(--font-display)} +.sidebar-version{padding:8px 16px 4px;font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--text3);font-family:var(--font-mono)} +.navbtn{display:flex;align-items:center;gap:8px;padding:8px 16px;border-radius:0;border:none;border-left:2px solid transparent;cursor:pointer;background:transparent;color:var(--sidebar-text);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:none;font-family:var(--font-body);width:100%;text-align:left;box-sizing:border-box;text-decoration:none;margin-bottom:1px;transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)} .navbtn:hover{background:var(--sidebar-hover-bg);color:var(--sidebar-text-hover);text-decoration:none} -.navbtn.active{background:var(--accent);color:#fff} -.sidebar-nav-section{padding:20px 12px 8px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--text3)} +.navbtn.active{background:var(--surface-hover);color:var(--sidebar-text-hover);border-left-color:var(--accent)} +.sidebar-nav-section{padding:16px 16px 4px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.14em;color:var(--sidebar-section);font-family:var(--font-mono)} .topnav-crumb{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .topnav-crumb a{color:var(--text3)} .topnav-crumb a:hover{color:var(--accent)} .topnav-crumb-sep{color:var(--border);margin:0 2px} .version-switcher{position:relative;flex:none;margin-left:auto} -.version-switcher-trigger{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--radius-pill);border:1px solid var(--border-on-light);background:var(--gray-100);cursor:pointer;list-style:none;font:var(--text-mono-label);letter-spacing:var(--tracking-mono-label);text-transform:uppercase;color:var(--gray-700)} +.version-switcher-trigger{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--radius-pill);border:1px solid var(--border);background:var(--gray-100);cursor:pointer;list-style:none;font:var(--text-mono-label);letter-spacing:var(--tracking-mono-label);text-transform:uppercase;color:var(--text2)} .version-switcher-trigger::-webkit-details-marker{display:none} -.version-switcher-trigger:hover{background:var(--gray-200)} -details.version-switcher[open]>.version-switcher-trigger{background:var(--gray-200)} -.version-switcher-trigger:focus-visible{outline:2px solid var(--purple);outline-offset:2px} -.version-switcher-chevron{transition:transform .15s ease} +.version-switcher-trigger:hover{background:var(--surface-hover)} +details.version-switcher[open]>.version-switcher-trigger{background:var(--surface-hover)} +.version-switcher-trigger:focus-visible{outline:2px solid var(--accent);outline-offset:2px} +.version-switcher-chevron{transition:transform var(--dur-normal) var(--ease)} details.version-switcher[open]>.version-switcher-trigger .version-switcher-chevron{transform:rotate(180deg)} -.version-switcher-menu{position:absolute;right:0;top:calc(100% + 8px);background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-card);border:1px solid var(--border-on-light);min-width:220px;max-height:320px;overflow-y:auto;padding:6px;z-index:250} -.version-switcher-item{display:block;padding:9px 12px;border-radius:var(--radius-sm);font:13px var(--font-body);color:var(--gray-700);text-decoration:none;margin-bottom:2px} +.version-switcher-menu{position:absolute;right:0;top:calc(100% + 8px);background:var(--surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-card);border:1px solid var(--border);min-width:220px;max-height:320px;overflow-y:auto;padding:6px;z-index:250} +.version-switcher-item{display:block;padding:9px 12px;border-radius:var(--radius-sm);font:13px var(--font-body);color:var(--text2);text-decoration:none;margin-bottom:2px} .version-switcher-item:last-child{margin-bottom:0} -.version-switcher-item:hover{background:var(--gray-100);text-decoration:none} -.version-switcher-item:focus-visible{outline:2px solid var(--purple);outline-offset:-2px} -.version-switcher-item.is-current{color:var(--purple);font-weight:600;background:var(--gray-100)} -.version-switcher-static{display:inline-flex;align-items:center;padding:6px 14px;border-radius:var(--radius-pill);background:var(--gray-100);font:var(--text-mono-label);letter-spacing:var(--tracking-mono-label);text-transform:uppercase;color:var(--gray-700);flex:none;margin-left:auto} +.version-switcher-item:hover{background:var(--surface-hover);text-decoration:none} +.version-switcher-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px} +.version-switcher-item.is-current{color:var(--accent);font-weight:600;background:var(--surface-hover)} +.version-switcher-static{display:inline-flex;align-items:center;padding:6px 14px;border-radius:var(--radius-pill);background:var(--gray-100);font:var(--text-mono-label);letter-spacing:var(--tracking-mono-label);text-transform:uppercase;color:var(--text2);flex:none;margin-left:auto} `; // --------------------------------------------------------------------------- @@ -445,6 +454,53 @@ function copyBtn(sig) { return ''; } +// --------------------------------------------------------------------------- +// Icons -- pure line SVGs (lucide paths, stroke-width 1.75), per design-system +// §6.1 ("zero filled variations, no emojis, visual cohesion with Lucide"). +// Replaces the old unicode-glyph icons (▶, ⚠) site-wide. +// --------------------------------------------------------------------------- + +var ICON_PATHS = { + search: '', + "chevron-right": '', + "alert-triangle": + '', + copy: '', + "arrow-up-right": '', + "file-x": + '', + "external-link": + '', + check: '', +}; + +/** + * Renders a named lucide-style line icon as inline SVG (design-system + * §6.1: pure SVG, `stroke-width:1.75`, rounded caps/joins, 24x24 default + * bounding box). Unknown names render nothing rather than throwing -- + * callers pass a literal string, so a typo should degrade silently, not + * break page render. + * @param {string} name - one of ICON_PATHS's keys. + * @param {number} [size] - width/height in px, default 16. + * @param {string} [cls] - optional extra CSS class on the . + * @returns {string} + */ +function icon(name, size, cls) { + var d = ICON_PATHS[name]; + if (!d) return ""; + return ( + '" + ); +} + /** * True when `s` is a `--fix`-generated placeholder, per `lib/fix.js`'s own * literal templates (`TODO_DESCRIPTION`, `TODO_RETURNS_DESCRIPTION`, the @@ -483,7 +539,7 @@ function metaHtml(item) { var out = ""; if (item.deprecated != null) out += - '
⚠ Deprecated' + + '
' + icon("alert-triangle", 13) + " Deprecated" + (item.deprecated ? ": " + esc(item.deprecated) : "") + "
"; if (item.since) out += '
Since v' + esc(item.since) + "
"; @@ -599,6 +655,13 @@ function resolveLinks(text, symbolMap, filePath, moduleHtmlPathFn, modules) { function sourceLink(item, filePath, sourceUrl) { if (item.line == null) return ""; var label = "line " + item.line; + // Defensive: filePath should always be the owning module's mod.filePath, + // but a malformed/synthetic module entry (e.g. from an upstream + // extraction edge case) could theoretically omit it -- degrade to a + // filePath-less line label rather than throwing and aborting the whole + // site build over one bad item (found during phase-o visual QA; not + // this pass's scope to root-cause the upstream data gap, see lessons.md). + if (!filePath) return '' + esc(label) + ""; if (sourceUrl) { var base = sourceUrl.replace(/\/$/, ""); var rel = filePath.replace(/\\/g, "/"); @@ -1230,8 +1293,8 @@ function buildTopnav(crumbHtml, versionSwitcherHtml) { '' + '' + (crumbHtml || "") + '' + '" + @@ -1535,9 +1598,15 @@ function buildSidebar(modules, projectName, version, activePath, rootPrefix, act })(tree); } var ancestorsOfActive = activeModule ? ancestorChain(tree, activeModule) : []; - var brand = '' + var brand = '' + (version ? '' : ""); var navButtons = 'Overview' + // task-arch-04: inserted directly after Overview, before Code Health (UX + // \u00a72.0 "directly under Overview" -- concatenation order == DOM order here). + // Gated on opts.architectureHref, same presence-gate pattern as qualityHref. + + (opts.architectureHref ? 'Architecture' : "") + (opts.qualityHref ? '\u2733 Code Health' : ""); var html = '