From 76a7a57f06876b3dbe7eb3e5357dddc523d756cc Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sat, 18 Oct 2025 00:36:43 -0500 Subject: [PATCH] feat(wip): prototyping classless css add-on --- cli/src/commands/css-docs.ts | 409 +++++++++++++ cli/src/commands/docs.ts | 158 +++-- cli/src/commands/stats.ts | 40 +- cli/src/console/echo.ts | 40 ++ cli/src/index.ts | 18 +- docs/css/semantics.md | 338 +++++++++++ docs/css/volt-css.md | 329 +++++++++++ index.html | 102 ++-- src/styles/base.css | 1070 ++++++++++++++++++++++++++++++++++ 9 files changed, 2338 insertions(+), 166 deletions(-) create mode 100644 cli/src/commands/css-docs.ts create mode 100644 cli/src/console/echo.ts create mode 100644 docs/css/semantics.md create mode 100644 docs/css/volt-css.md create mode 100644 src/styles/base.css diff --git a/cli/src/commands/css-docs.ts b/cli/src/commands/css-docs.ts new file mode 100644 index 0000000..62ec2b8 --- /dev/null +++ b/cli/src/commands/css-docs.ts @@ -0,0 +1,409 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { echo } from "../console/echo"; + +type CSSComment = { selector: string; comment: string }; +type CSSVariable = { name: string; value: string; category: string }; +type ElementCoverage = { element: string; covered: boolean }; + +/** + * Extract CSS doc comments from CSS file + * Parses block comments and associates them with selectors + */ +function extractCSSComments(cssContent: string): CSSComment[] { + const comments: CSSComment[] = []; + const lines = cssContent.split("\n"); + + let currentComment = ""; + let inComment = false; + let commentLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + if (trimmed.startsWith("/**") || trimmed.startsWith("/*")) { + inComment = true; + commentLines = []; + const commentText = trimmed.replace(/^\/\*+\s*/, "").replace(/\*\/\s*$/, ""); + if (commentText && !commentText.startsWith("=")) { + commentLines.push(commentText); + } + continue; + } + + if (inComment) { + if (trimmed.includes("*/")) { + const commentText = trimmed.replace(/\*\/.*$/, "").replace(/^\*\s*/, ""); + if (commentText && !commentText.startsWith("=")) { + commentLines.push(commentText); + } + currentComment = commentLines.join(" ").trim(); + inComment = false; + + for (let j = i + 1; j < lines.length; j++) { + const nextLine = lines[j].trim(); + if (nextLine === "" || nextLine.startsWith("/*")) { + continue; + } + + if (nextLine.includes("{") || j + 1 < lines.length && lines[j + 1].includes("{")) { + const selector = nextLine.replace("{", "").trim(); + if (selector && currentComment) { + comments.push({ selector, comment: currentComment }); + } + break; + } + break; + } + currentComment = ""; + continue; + } + + const commentText = trimmed.replace(/^\*\s*/, ""); + if (commentText && !commentText.startsWith("=")) { + commentLines.push(commentText); + } + } + } + + return comments; +} + +/** + * Extract CSS custom properties (variables) from :root + * Groups them by category based on naming conventions + */ +function extractCSSVariables(cssContent: string): CSSVariable[] { + const variables: CSSVariable[] = []; + const lines = cssContent.split("\n"); + let inRoot = false; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith(":root")) { + inRoot = true; + continue; + } + + if (inRoot && trimmed === "}") { + inRoot = false; + continue; + } + + if (inRoot && trimmed.startsWith("--")) { + const match = trimmed.match(/^(--[a-z0-9-]+)\s*:\s*([^;]+);/); + if (match) { + const [, name, value] = match; + const category = categorizeCSSVariable(name); + variables.push({ name, value: value.trim(), category }); + } + } + } + + return variables; +} + +/** + * Categorize CSS variable by name prefix + */ +function categorizeCSSVariable(name: string): string { + if (name.startsWith("--font")) return "Typography"; + if (name.startsWith("--line-height")) return "Typography"; + if (name.startsWith("--space")) return "Spacing"; + if (name.startsWith("--color")) return "Colors"; + if (name.startsWith("--shadow")) return "Effects"; + if (name.startsWith("--radius")) return "Effects"; + if (name.startsWith("--transition")) return "Effects"; + if (name.startsWith("--content")) return "Layout"; + if (name.startsWith("--sidenote")) return "Layout"; + return "Other"; +} + +/** + * Validate CSS element coverage + * Checks which HTML elements have styling defined + */ +function validateElementCoverage(cssContent: string): ElementCoverage[] { + const elementsToCheck = [ + "html", + "body", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "p", + "a", + "em", + "strong", + "mark", + "small", + "sub", + "sup", + "ul", + "ol", + "li", + "dl", + "dt", + "dd", + "blockquote", + "cite", + "code", + "pre", + "kbd", + "samp", + "var", + "hr", + "table", + "thead", + "tbody", + "th", + "td", + "tr", + "form", + "fieldset", + "legend", + "label", + "input", + "select", + "textarea", + "button", + "img", + "figure", + "figcaption", + "video", + "audio", + "canvas", + "svg", + "iframe", + "article", + "section", + "aside", + "header", + "footer", + "nav", + "details", + "summary", + ]; + + const coverage: ElementCoverage[] = []; + + for (const element of elementsToCheck) { + const patterns = [ + // element { + new RegExp(`^${element}\\s*\\{`, "m"), + // element, + new RegExp(`^${element},`, "m"), + // , element { + new RegExp(`,\\s*${element}\\s*\\{`, "m"), + // element:pseudo + new RegExp(`^${element}:`, "m"), + // element[attr] + new RegExp(`${element}\\[`, "m"), + ]; + + const covered = patterns.some((pattern) => pattern.test(cssContent)); + coverage.push({ element, covered }); + } + + return coverage; +} + +/** + * Generate markdown documentation from extracted data + */ +function generateSemanticsDocs(comments: CSSComment[], variables: CSSVariable[], coverage: ElementCoverage[]): string { + const lines: string[] = [ + "# Volt CSS Semantics", + "", + "Auto-generated documentation from base.css", + "", + "## CSS Custom Properties", + "", + "All design tokens defined in the stylesheet.", + "", + ]; + + const categoryMap = new Map(); + for (const variable of variables) { + if (!categoryMap.has(variable.category)) { + categoryMap.set(variable.category, []); + } + categoryMap.get(variable.category)!.push(variable); + } + + for (const [category, vars] of categoryMap) { + lines.push(`### ${category}`, ""); + for (const v of vars) { + lines.push(`- \`${v.name}\`: \`${v.value}\``); + } + lines.push(""); + } + + lines.push("## Element Coverage", "", "HTML elements with defined styling in the stylesheet.", ""); + + const covered = coverage.filter((c) => c.covered); + const notCovered = coverage.filter((c) => !c.covered); + + lines.push(`**Coverage**: ${covered.length}/${coverage.length} elements`, "", "### Styled Elements", ""); + + const coveredByCategory = groupElementsByCategory(covered.map((c) => c.element)); + for (const [category, elements] of Object.entries(coveredByCategory)) { + lines.push(`**${category}**: ${elements.join(", ")}`); + } + lines.push(""); + + if (notCovered.length > 0) { + lines.push("### Unstyled Elements", "", notCovered.map((c) => c.element).join(", "), ""); + } + + lines.push("## Documentation Comments", "", "Inline documentation extracted from CSS comments.", ""); + + for (const comment of comments) { + if (comment.comment.length > 200) { + continue; + } + + lines.push(`### \`${comment.selector}\``, ""); + lines.push(comment.comment, ""); + } + + return lines.join("\n"); +} + +/** + * Group HTML elements by category for better organization + */ +function groupElementsByCategory(elements: string[]): Record { + const categories: Record = { + "Document Structure": [], + "Typography": [], + "Lists": [], + "Semantic": [], + "Forms": [], + "Tables": [], + "Media": [], + "Code": [], + }; + + const categoryMap: Record = { + html: "Document Structure", + body: "Document Structure", + h1: "Typography", + h2: "Typography", + h3: "Typography", + h4: "Typography", + h5: "Typography", + h6: "Typography", + p: "Typography", + a: "Typography", + em: "Typography", + strong: "Typography", + mark: "Typography", + small: "Typography", + sub: "Typography", + sup: "Typography", + hr: "Typography", + ul: "Lists", + ol: "Lists", + li: "Lists", + dl: "Lists", + dt: "Lists", + dd: "Lists", + blockquote: "Semantic", + cite: "Semantic", + article: "Semantic", + section: "Semantic", + aside: "Semantic", + header: "Semantic", + footer: "Semantic", + nav: "Semantic", + details: "Semantic", + summary: "Semantic", + form: "Forms", + fieldset: "Forms", + legend: "Forms", + label: "Forms", + input: "Forms", + select: "Forms", + textarea: "Forms", + button: "Forms", + table: "Tables", + thead: "Tables", + tbody: "Tables", + th: "Tables", + td: "Tables", + tr: "Tables", + img: "Media", + figure: "Media", + figcaption: "Media", + video: "Media", + audio: "Media", + canvas: "Media", + svg: "Media", + iframe: "Media", + code: "Code", + pre: "Code", + kbd: "Code", + samp: "Code", + var: "Code", + }; + + for (const element of elements) { + const category = categoryMap[element] || "Other"; + if (!categories[category]) { + categories[category] = []; + } + categories[category].push(element); + } + + return Object.fromEntries(Object.entries(categories).filter(([, els]) => els.length > 0)); +} + +/** + * CSS documentation command implementation + * Generates semantics.md from base.css + */ +export async function cssDocsCommand(): Promise { + const projectRoot = path.join(process.cwd(), ".."); + const cssPath = path.join(projectRoot, "src", "styles", "base.css"); + const outputDir = path.join(projectRoot, "docs", "css"); + const outputPath = path.join(outputDir, "semantics.md"); + + echo.title("\nGenerating CSS Documentation\n"); + + let cssContent: string; + try { + cssContent = await readFile(cssPath, "utf8"); + echo.ok(`Read ${cssPath}`); + } catch (error) { + echo.err(`Failed to read CSS file: ${cssPath}`); + throw error; + } + + echo.info("\nExtracting CSS documentation..."); + + const comments = extractCSSComments(cssContent); + echo.ok(` Found ${comments.length} documented selectors`); + + const variables = extractCSSVariables(cssContent); + echo.ok(` Found ${variables.length} CSS custom properties`); + + const coverage = validateElementCoverage(cssContent); + const coveredCount = coverage.filter((c) => c.covered).length; + echo.ok(` Element coverage: ${coveredCount}/${coverage.length}`); + + const markdown = generateSemanticsDocs(comments, variables, coverage); + + await mkdir(outputDir, { recursive: true }); + await writeFile(outputPath, markdown, "utf8"); + + echo.success(`\nCSS documentation generated: docs/css/semantics.md\n`); + echo.label("Summary:"); + echo.text(` CSS Comments: ${comments.length}`); + echo.text(` CSS Variables: ${variables.length}`); + echo.text(` Element Coverage: ${coveredCount}/${coverage.length}\n`); +} diff --git a/cli/src/commands/docs.ts b/cli/src/commands/docs.ts index e065eef..77ca419 100644 --- a/cli/src/commands/docs.ts +++ b/cli/src/commands/docs.ts @@ -1,9 +1,10 @@ -import chalk from "chalk"; import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import ts from "typescript"; +import { echo } from "../console/echo.js"; type Member = { name: string; type: string; docs?: string }; + type EntryKind = "function" | "interface" | "type" | "class"; type DocumentEntry = { @@ -20,7 +21,7 @@ type JSDocumentParsed = { description: string; examples: string[] }; /** * Extract and parse JSDoc comment text */ -function extractJSDocument(node: ts.Node, sourceFile: ts.SourceFile): JSDocumentParsed { +function extractJSDoc(node: ts.Node, sourceFile: ts.SourceFile): JSDocumentParsed { const fullText = sourceFile.getFullText(); const ranges = ts.getLeadingCommentRanges(fullText, node.getFullStart()); @@ -79,7 +80,7 @@ function extractJSDocument(node: ts.Node, sourceFile: ts.SourceFile): JSDocument /** * Extract function signature */ -function extractFunctionSignature(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): string { +function extractFnSig(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): string { const start = node.getStart(sourceFile); const end = node.body ? node.body.getStart(sourceFile) : node.getEnd(); return sourceFile.text.substring(start, end).trim().replaceAll(/\s+/g, " "); @@ -88,17 +89,14 @@ function extractFunctionSignature(node: ts.FunctionDeclaration, sourceFile: ts.S /** * Extract interface members */ -function extractInterfaceMembers( - node: ts.InterfaceDeclaration, - sourceFile: ts.SourceFile, -): Array<{ name: string; type: string; docs?: string }> { - const members: Array<{ name: string; type: string; docs?: string }> = []; +function extractIMembers(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): Array { + const members: Array = []; for (const member of node.members) { if (ts.isPropertySignature(member) && member.name) { const name = member.name.getText(sourceFile); const type = member.type ? member.type.getText(sourceFile) : "unknown"; - const { description } = extractJSDocument(member, sourceFile); + const { description } = extractJSDoc(member, sourceFile); members.push({ name, type, docs: description || undefined }); } @@ -107,64 +105,10 @@ function extractInterfaceMembers( return members; } -/** - * Parse a TypeScript file and extract documentation - */ -function parseFile(filePath: string, content: string): DocumentEntry[] { - const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); - const entries: DocumentEntry[] = []; - - function visit(node: ts.Node) { - if (ts.isFunctionDeclaration(node) && node.name) { - const modifiers = node.modifiers; - const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); - - if (isExported) { - const name = node.name.text; - const { description, examples } = extractJSDocument(node, sourceFile); - const signature = extractFunctionSignature(node, sourceFile); - - entries.push({ name, kind: "function", description, examples, signature }); - } - } - - if (ts.isInterfaceDeclaration(node)) { - const modifiers = node.modifiers; - const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); - - if (isExported) { - const name = node.name.text; - const { description, examples } = extractJSDocument(node, sourceFile); - const members = extractInterfaceMembers(node, sourceFile); - - entries.push({ name, kind: "interface", description, examples, members }); - } - } - - if (ts.isTypeAliasDeclaration(node)) { - const modifiers = node.modifiers; - const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); - - if (isExported) { - const name = node.name.text; - const { description, examples } = extractJSDocument(node, sourceFile); - const signature = node.type.getText(sourceFile); - - entries.push({ name, kind: "type", description, examples, signature }); - } - } - - ts.forEachChild(node, visit); - } - - visit(sourceFile); - return entries; -} - /** * Generate markdown for a documentation entry */ -function generateMarkdown(entries: DocumentEntry[], moduleName: string, moduleDocs: string): string { +function generateMD(entries: DocumentEntry[], moduleName: string, moduleDocs: string): string { const lines: string[] = []; lines.push(`# ${moduleName}`, ""); @@ -210,32 +154,86 @@ function generateMarkdown(entries: DocumentEntry[], moduleName: string, moduleDo /** * Extract module-level documentation */ -function extractModuleDocs(content: string): string { +function extractModDocs(content: string): string { const lines = content.split("\n"); - const documentLines: string[] = []; - let inDocument = false; + const docLines: string[] = []; + let inDoc = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed === "/**") { - inDocument = true; + inDoc = true; continue; } - if (inDocument) { + if (inDoc) { if (trimmed === "*/") { break; } const cleaned = trimmed.replace(/^\*\s?/, ""); if (!cleaned.startsWith("@packageDocumentation")) { - documentLines.push(cleaned); + docLines.push(cleaned); } } } - return documentLines.join("\n").trim(); + return docLines.join("\n").trim(); +} + +/** + * Parse a TypeScript file and extract documentation + */ +function parseFile(filePath: string, content: string): DocumentEntry[] { + const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); + const entries: DocumentEntry[] = []; + + function visit(node: ts.Node) { + if (ts.isFunctionDeclaration(node) && node.name) { + const modifiers = node.modifiers; + const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + + if (isExported) { + const name = node.name.text; + const { description, examples } = extractJSDoc(node, sourceFile); + const signature = extractFnSig(node, sourceFile); + + entries.push({ name, kind: "function", description, examples, signature }); + } + } + + if (ts.isInterfaceDeclaration(node)) { + const modifiers = node.modifiers; + const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + + if (isExported) { + const name = node.name.text; + const { description, examples } = extractJSDoc(node, sourceFile); + const members = extractIMembers(node, sourceFile); + + entries.push({ name, kind: "interface", description, examples, members }); + } + } + + if (ts.isTypeAliasDeclaration(node)) { + const modifiers = node.modifiers; + const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + + if (isExported) { + const name = node.name.text; + const { description, examples } = extractJSDoc(node, sourceFile); + const signature = node.type.getText(sourceFile); + + entries.push({ name, kind: "type", description, examples, signature }); + } + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return entries; } /** @@ -249,15 +247,15 @@ async function processFile(filePath: string, baseDir: string, outputDir: string) return; } - const moduleDocs = extractModuleDocs(content); + const moduleDocs = extractModDocs(content); const relativePath = path.relative(baseDir, filePath); const moduleName = path.basename(relativePath, ".ts"); - const markdown = generateMarkdown(entries, moduleName, moduleDocs); + const markdown = generateMD(entries, moduleName, moduleDocs); const outputPath = path.join(outputDir, `${moduleName}.md`); await writeFile(outputPath, markdown, "utf8"); - console.log(chalk.green(` Generated: ${relativePath} -> api/${moduleName}.md`)); + echo.ok(` Generated: ${relativePath} -> api/${moduleName}.md`); } /** @@ -283,21 +281,21 @@ async function findTsFiles(dir: string, files: string[] = []): Promise * Docs command implementation */ export async function docsCommand(): Promise { - const projectRoot = path.join(process.cwd(), ".."); - const srcDir = path.join(projectRoot, "src"); - const docsDir = path.join(projectRoot, "docs", "api"); + const root = path.join(process.cwd(), ".."); + const srcDir = path.join(root, "src"); + const docsDir = path.join(root, "docs", "api"); - console.log(chalk.blue.bold("\nGenerating API Documentation\n")); + echo.title("\nGenerating API Documentation\n"); await mkdir(docsDir, { recursive: true }); const files = await findTsFiles(srcDir); - console.log(chalk.cyan(`Found ${files.length} TypeScript files\n`)); + echo.info(`Found ${files.length} TypeScript files\n`); for (const file of files) { await processFile(file, srcDir, docsDir); } - console.log(chalk.green.bold(`\nAPI documentation generated in docs/api/\n`)); + echo.success(`\nAPI documentation generated in docs/api/\n`); } diff --git a/cli/src/commands/stats.ts b/cli/src/commands/stats.ts index efbe1dd..67dfbbb 100644 --- a/cli/src/commands/stats.ts +++ b/cli/src/commands/stats.ts @@ -1,6 +1,6 @@ -import chalk from "chalk"; import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; +import { echo } from "../console/echo.js"; type FileStats = { path: string; lines: number; totalLines: number }; type DirectoryStats = { totalLines: number; codeLines: number; files: FileStats[] }; @@ -93,15 +93,15 @@ export async function statsCommand(includeFull: boolean): Promise { const srcDir = path.join(projectRoot, "src"); const testDir = path.join(projectRoot, "test"); - console.log(chalk.blue.bold("\nVolt.js Code Statistics\n")); + echo.title("\nVolt.js Code Statistics\n"); const srcStats = await collectStats(srcDir, projectRoot); - console.log(chalk.cyan("Source Code (src/):")); - console.log(` Files: ${srcStats.files.length}`); - console.log(` Total Lines: ${srcStats.totalLines}`); - console.log(chalk.green(` Code Lines: ${srcStats.codeLines}`)); - console.log(` Doc/Comments: ${srcStats.totalLines - srcStats.codeLines}`); + echo.label("Source Code (src/):"); + echo.text(` Files: ${srcStats.files.length}`); + echo.text(` Total Lines: ${srcStats.totalLines}`); + echo.ok(` Code Lines: ${srcStats.codeLines}`); + echo.text(` Doc/Comments: ${srcStats.totalLines - srcStats.codeLines}`); let totalCode = srcStats.codeLines; let totalTotal = srcStats.totalLines; @@ -111,29 +111,29 @@ export async function statsCommand(includeFull: boolean): Promise { if (includeFull) { const testStats = await collectStats(testDir, projectRoot); - console.log(chalk.cyan("\nTest Code (test/):")); - console.log(` Files: ${testStats.files.length}`); - console.log(` Total Lines: ${testStats.totalLines}`); - console.log(chalk.green(` Code Lines: ${testStats.codeLines}`)); - console.log(` Doc/Comments: ${testStats.totalLines - testStats.codeLines}`); + echo.label("\nTest Code (test/):"); + echo.text(` Files: ${testStats.files.length}`); + echo.text(` Total Lines: ${testStats.totalLines}`); + echo.ok(` Code Lines: ${testStats.codeLines}`); + echo.text(` Doc/Comments: ${testStats.totalLines - testStats.codeLines}`); totalCode += testStats.codeLines; totalTotal += testStats.totalLines; totalFileCount += testStats.files.length; } - console.log(chalk.blue.bold("\nTotal:")); - console.log(` Files: ${totalFileCount}`); - console.log(` Total Lines: ${totalTotal}`); - console.log(chalk.green.bold(` Code Lines: ${totalCode}`)); - console.log(` Doc/Comments: ${totalTotal - totalCode}`); + echo.title("\nTotal:"); + echo.text(` Files: ${totalFileCount}`); + echo.text(` Total Lines: ${totalTotal}`); + echo.success(` Code Lines: ${totalCode}`); + echo.text(` Doc/Comments: ${totalTotal - totalCode}`); if (process.env.VERBOSE) { - console.log(chalk.yellow("\n\nFile Breakdown:")); + echo.warn("\n\nFile Breakdown:"); for (const file of srcStats.files) { - console.log(` ${file.path}: ${file.lines} lines`); + echo.text(` ${file.path}: ${file.lines} lines`); } } - console.log(); + echo.text(); } diff --git a/cli/src/console/echo.ts b/cli/src/console/echo.ts new file mode 100644 index 0000000..2ebc96a --- /dev/null +++ b/cli/src/console/echo.ts @@ -0,0 +1,40 @@ +import chalk from "chalk"; + +type Echo = Record< + "info" | "success" | "ok" | "warn" | "text" | "err" | "danger" | "label" | "title", + (message?: any, ...optionalParams: any[]) => void +>; + +export const echo: Echo = { + /** + * Red text to stderr + */ + err(message, ...optionalParams) { + console.error(chalk.red(message), ...optionalParams); + }, + /** + * Red text for recoverable errors (to stdout) + */ + danger(message, ...optionalParams) { + console.log(chalk.red(message), ...optionalParams); + }, + ok(message, ...optionalParams) { + console.log(chalk.green(message), ...optionalParams); + }, + success(message, ...optionalParams) { + console.log(chalk.green.bold(message), ...optionalParams); + }, + info(message, ...optionalParams) { + console.log(chalk.cyan(message), ...optionalParams); + }, + label(message, ...optionalParams) { + console.log(chalk.blue(message), ...optionalParams); + }, + title(message, ...optionalParams) { + console.log(chalk.blue.bold(message), ...optionalParams); + }, + warn(message, ...optionalParams) { + console.warn(chalk.yellow(message), ...optionalParams); + }, + text: console.log, +}; diff --git a/cli/src/index.ts b/cli/src/index.ts index 05645ff..bc68f06 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,7 +1,8 @@ -import chalk from "chalk"; import { Command } from "commander"; +import { cssDocsCommand } from "./commands/css-docs.js"; import { docsCommand } from "./commands/docs.js"; import { statsCommand } from "./commands/stats.js"; +import { echo } from "./console/echo.js"; const program = new Command(); @@ -11,7 +12,7 @@ program.command("docs").description("Generate API documentation from TypeScript try { await docsCommand(); } catch (error) { - console.error(chalk.red("Error generating docs:"), error); + echo.err("Error generating docs:", error); process.exit(1); } }); @@ -23,9 +24,20 @@ program.command("stats").description("Display lines of code statistics").option( try { await statsCommand(options.full); } catch (error) { - console.error(chalk.red("Error generating stats:"), error); + echo.err("Error generating stats:", error); process.exit(1); } }); +program.command("css-docs").description("Generate CSS documentation from base.css comments and variables").action( + async () => { + try { + await cssDocsCommand(); + } catch (error) { + echo.err("Error generating CSS docs:", error); + process.exit(1); + } + }, +); + program.parse(); diff --git a/docs/css/semantics.md b/docs/css/semantics.md new file mode 100644 index 0000000..f71836f --- /dev/null +++ b/docs/css/semantics.md @@ -0,0 +1,338 @@ +# Volt CSS Semantics + +Auto-generated documentation from base.css + +## CSS Custom Properties + +All design tokens defined in the stylesheet. + +### Typography + +- `--font-size-base`: `18px` +- `--font-size-sm`: `0.889rem` +- `--font-size-lg`: `1.125rem` +- `--font-size-xl`: `1.266rem` +- `--font-size-2xl`: `1.424rem` +- `--font-size-3xl`: `1.802rem` +- `--font-size-4xl`: `2.027rem` +- `--font-size-5xl`: `2.566rem` +- `--font-sans`: `"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` +- `--font-serif`: `"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif` +- `--font-mono`: `"SF Mono", "Cascadia Code", "Fira Code", "Roboto Mono", Consolas, monospace` +- `--line-height-tight`: `1.25` +- `--line-height-base`: `1.6` +- `--line-height-relaxed`: `1.8` +- `--font-size-base`: `16px` +- `--font-size-base`: `15px` + +### Spacing + +- `--space-xs`: `0.25rem` +- `--space-sm`: `0.5rem` +- `--space-md`: `1rem` +- `--space-lg`: `1.5rem` +- `--space-xl`: `2rem` +- `--space-2xl`: `3rem` +- `--space-3xl`: `4rem` +- `--space-2xl`: `2rem` +- `--space-3xl`: `3rem` + +### Layout + +- `--content-width`: `70ch` +- `--sidenote-width`: `18rem` +- `--sidenote-gap`: `2rem` + +### Colors + +- `--color-bg`: `#fefefe` +- `--color-bg-alt`: `#f5f5f5` +- `--color-text`: `#1a1a1a` +- `--color-text-muted`: `#666666` +- `--color-accent`: `#0066cc` +- `--color-accent-hover`: `#0052a3` +- `--color-border`: `#d4d4d4` +- `--color-code-bg`: `#f8f8f8` +- `--color-mark`: `#fff3cd` +- `--color-success`: `#22863a` +- `--color-warning`: `#bf8700` +- `--color-error`: `#cb2431` +- `--color-bg`: `#1a1a1a` +- `--color-bg-alt`: `#2a2a2a` +- `--color-text`: `#e6e6e6` +- `--color-text-muted`: `#a0a0a0` +- `--color-accent`: `#4da6ff` +- `--color-accent-hover`: `#66b3ff` +- `--color-border`: `#404040` +- `--color-code-bg`: `#2a2a2a` +- `--color-mark`: `#4a4a00` +- `--color-success`: `#34d058` +- `--color-warning`: `#ffdf5d` +- `--color-error`: `#f97583` + +### Effects + +- `--shadow-sm`: `0 1px 2px rgba(0, 0, 0, 0.05)` +- `--shadow-md`: `0 4px 6px rgba(0, 0, 0, 0.07)` +- `--shadow-lg`: `0 10px 15px rgba(0, 0, 0, 0.1)` +- `--radius-sm`: `3px` +- `--radius-md`: `6px` +- `--radius-lg`: `8px` +- `--transition-fast`: `150ms ease-in-out` +- `--transition-base`: `250ms ease-in-out` +- `--shadow-sm`: `0 1px 2px rgba(0, 0, 0, 0.3)` +- `--shadow-md`: `0 4px 6px rgba(0, 0, 0, 0.4)` +- `--shadow-lg`: `0 10px 15px rgba(0, 0, 0, 0.5)` + +## Element Coverage + +HTML elements with defined styling in the stylesheet. + +**Coverage**: 58/60 elements + +### Styled Elements + +**Document Structure**: html, body +**Typography**: h1, h2, h3, h4, h5, h6, p, a, em, strong, mark, small, sub, sup, hr +**Lists**: ul, ol, li, dl, dt, dd +**Semantic**: blockquote, cite, article, section, aside, header, footer, nav, details, summary +**Forms**: form, fieldset, legend, label, input, select, textarea, button +**Tables**: table, thead, th, td +**Media**: img, figure, figcaption, video, audio, canvas, svg, iframe +**Code**: code, pre, kbd, samp, var + +### Unstyled Elements + +tbody, tr + +## Documentation Comments + +Inline documentation extracted from CSS comments. + +### `:root` + +Root-level CSS variables define the design system. Light theme is default, dark theme overrides via media query. + +### `@media (prefers-color-scheme: dark)` + +Dark Theme Overrides Automatically applied when user prefers dark color scheme + +### `*, *::before, *::after` + +Modern CSS reset with sensible defaults + +### `html` + +Document root configuration Sets base font size for rem calculations + +### `body` + +Body element - Primary container Sets default typography and colors for the entire document + +### `h1, h2, h3, h4, h5, h6` + +Headings hierarchy Uses modular scale for harmonious sizing Tighter line-height for larger text improves readability + +### `h1` + +Individual heading sizes h1-h3 use slightly larger weights for emphasis + +### `p` + +Paragraph spacing Generous spacing between paragraphs aids scanning + +### `h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p` + +First paragraph after headings - No top margin Common convention in academic typography + +### `a` + +Links - Accessible and distinctive Uses accent color with underline for clarity + +### `em` + +Emphasis and strong elements + +### `mark` + +Marked/highlighted text + +### `sub, sup` + +Subscript and superscript Prevents them from affecting line height + +### `small` + +Small text Also used for Tufte-style sidenotes (see sidenotes section) + +### `ul, ol` + +List spacing and indentation Nested lists inherit proper spacing + +### `li` + +List items + +### `li > ul, li > ol` + +Nested lists - Reduced spacing + +### `dl` + +Description lists - For key-value pairs + +### `p:has(small)` + +Parent paragraph must be positioned for absolute children + +### `p small` + +Pull small elements into the right margin Creates classic Tufte-style sidenote layout + +### `@media (max-width: 767px)` + +Mobile sidenotes - Inline with subtle styling + +### `blockquote` + +Blockquote styling Left border for visual distinction, italic for emphasis + +### `cite` + +Citation element + +### `code` + +Inline code Monospace font with subtle background for distinction + +### `kbd` + +Keyboard input Styled like keys on a keyboard + +### `samp` + +Sample output + +### `var` + +Variable + +### `pre` + +Preformatted code blocks Horizontal scrolling for overflow, no word wrap + +### `hr` + +Section dividers Centered decorative element with breathing room + +### `table` + +Table container for horizontal scrolling on small screens + +### `thead` + +Table header styling Bold text with bottom border for separation + +### `td` + +Table cells + +### `tbody tr:nth-child(even)` + +Zebra striping for easier row scanning + +### `tbody tr:hover` + +Hover state for interactive tables + +### `form` + +Form container spacing + +### `fieldset` + +Fieldset grouping + +### `label` + +Labels Block display for better touch targets + +### `textarea` + +Textarea specific + +### `input[type="checkbox"],` + +Checkboxes and radio buttons + +### `input[type="file"]` + +File input + +### `input[type="range"]` + +Range input + +### `progress, meter` + +Progress and meter + +### `input[type="reset"]` + +Reset button - Subdued styling + +### `img` + +Images Responsive by default, maintains aspect ratio + +### `figure` + +Figures with captions Common in academic and technical writing + +### `video, audio` + +Video and audio Responsive and accessible + +### `canvas, svg` + +Canvas and SVG + +### `iframe` + +iframe - Responsive wrapper + +### `article, section` + +Article and Section Spacing between major content blocks + +### `aside` + +Aside Complementary content, styled distinctly + +### `header` + +Header and Footer + +### `nav` + +Nav Navigation menus + +### `details` + +Details and Summary Disclosure widget for expandable content + +### `.sr-only` + +Screen reader only Hides content visually but keeps it accessible to assistive technology + +### `@media print` + +Print-specific optimizations + +### `@media (max-width: 768px)` + +Tablet and below - Reduce spacing + +### `@media (max-width: 480px)` + +Mobile - Further reduced spacing and sizing diff --git a/docs/css/volt-css.md b/docs/css/volt-css.md new file mode 100644 index 0000000..286f379 --- /dev/null +++ b/docs/css/volt-css.md @@ -0,0 +1,329 @@ +# Volt CSS + +A classless CSS stylesheet for elegant, readable web documents. Drop it into any HTML page for instant, semantic styling without touching a single class name. + +## Philosophy + +Volt CSS embraces semantic HTML5 and lets the content structure define the presentation. Inspired by academic typography and modern web design, it creates documents that are beautiful, accessible, and optimized for reading. + +### Core Principles + +- **Classless**: Style semantic HTML elements directly. +- Optimized line lengths, modular type scale, and generous whitespace optimized for reading +- Automatic light and dark modes via `prefers-color-scheme` to respect user preferences with carefully calibrated color palettes for both modes. +- **Accessibility**: WCAG AA contrast ratios, keyboard navigation support, and semantic HTML patterns +- Mobile-first (ish) design that adapts gracefully from phones to wide desktop monitors without compromising readability. + +## Inspiration + +Volt CSS synthesizes ideas from several excellent classless CSS frameworks: + +- **magick.css**: Tufte-style [sidenotes](#tufte-style-sidenotes), playful personality, well-commented code +- **LaTeX.css**: Academic typography, wide margins, optimized for technical content +- **Sakura**: Minimal duotone color palettes, rapid prototyping +- **Matcha**: Semantic hierarchy, CSS custom properties architecture +- **MVP.css**: Sensible defaults, zero configuration needed + +## Features + +### Complete Element Coverage + +All semantic HTML5 elements are styled out of the box: + +- Typography: headings, paragraphs, links, lists (ordered, unordered, description) +- Content: blockquotes, code blocks, tables, figures with captions +- Forms: inputs, textareas, selects, buttons, checkboxes, radio buttons, file uploads +- Media: images, video, audio, iframes +- Semantic: article, section, aside, header, footer, nav, details/summary + +### Tufte-Style Sidenotes + +Inspired by Edward Tufte's beautiful book design, margin notes can be added using simple `` elements within paragraphs. + +**Desktop**: Notes appear in the right margin +**Mobile**: Notes appear inline with subtle styling + +### Example + +```html +

+ The framework handles reactivity through signals. + + Signals are similar to reactive primitives in Solid.js and Vue 3's + ref() system, but with a simpler API surface. + + This approach keeps the mental model straightforward. +

+``` + +### Modular Type Scale + +Font sizes use a 1.25 ratio (major third) for "harmonious" hierarchy: + +- Base: `18px` (`1rem`) +- Scale: `0.889rem`, `1.125rem`, `1.266rem`, `1.424rem`, `1.802rem`, `2.027rem`, `2.566rem` +- Headings use larger sizes from the scale, body text uses base and smaller sizes + +### Optimized Reading Width + +Main content is constrained to approximately 70 characters per line, around the optimal range for comfortable reading. +Sidenotes extend into the right margin when space allows. + +### Dark Mode Support + +The stylesheet automatically switches to dark mode when the user's system preference is set to dark: + +```css +@media (prefers-color-scheme: dark) { + /* Dark theme colors applied automatically */ +} +``` + +Both themes use carefully selected colors with proper contrast ratios for accessibility. + +## Usage + +### Basic Setup + +Include the stylesheet in your HTML ``: + +```html + + + + + + + My Document + + + + + +``` + +### Example Document Structure + +```html + +
+

Document Title

+

Subtitle or introduction

+
+ +
+

Section Heading

+

+ Your content flows naturally. + Add sidenotes for additional context. + The stylesheet handles all the styling. +

+ +
+

Quotes are styled with subtle backgrounds and borders.

+ Author Name +
+ +
// Code blocks use monospace fonts
+const example = "syntax highlighting not included";
+ + + + + + + + + + + + + + +
FeatureStatus
TablesStyled with zebra striping
+
+ +
+

Footer content, copyright, etc.

+
+ +``` + +### Forms + +Forms get styling with focus states, required field indicators, and proper spacing: + +```html +
+
+ User Information + + + + + + + + + + + + +
+
+``` + +## Customization + +### CSS Custom Properties + +All design tokens are defined as CSS custom properties (CSS variables) in the `:root` selector. Override them to customize the appearance: + +```css +:root { + /* Change the accent color */ + --color-accent: #d63384; + --color-accent-hover: #b02a6b; + + /* Adjust spacing */ + --space-md: 1.25rem; + + /* Change fonts */ + --font-sans: "Your Font", system-ui, sans-serif; + + /* Modify content width */ + --content-width: 60ch; +} +``` + +### Properties + +**Typography**: + +- `--font-sans`, `--font-serif`, `--font-mono`: Font families +- `--font-size-*`: Size scale from sm to 5xl +- `--line-height-tight`, `--line-height-base`, `--line-height-relaxed` + +**Colors**: + +- `--color-bg`: Background color +- `--color-bg-alt`: Alternate background (code blocks, table stripes) +- `--color-text`: Primary text color +- `--color-text-muted`: Secondary text color +- `--color-accent`: Accent color for links, buttons +- `--color-accent-hover`: Hover state for accent color +- `--color-border`: Border color +- `--color-code-bg`: Code block background +- `--color-mark`: Highlighted text background +- `--color-success`, `--color-warning`, `--color-error`: Semantic colors + +**Spacing**: + +- `--space-xs` through `--space-3xl`: Spacing scale + +**Layout**: + +- `--content-width`: Maximum width for readable content +- `--sidenote-width`: Width of margin notes +- `--sidenote-gap`: Space between content and sidenotes + +**Effects**: + +- `--shadow-sm`, `--shadow-md`, `--shadow-lg`: Box shadows +- `--radius-sm`, `--radius-md`, `--radius-lg`: Border radius +- `--transition-fast`, `--transition-base`: Transition durations + +### Dark Mode Customization + +Override dark mode colors specifically: + +```css +@media (prefers-color-scheme: dark) { + :root { + --color-accent: #f0f; + --color-bg: #000; + } +} +``` + +### Scoped Customization + +Apply custom styling to specific sections without affecting the whole document: + +```html + + +
+

This section uses different colors and fonts

+

All nested elements inherit the custom properties.

+
+``` + +## Browser Support + +Volt CSS uses modern CSS features and targets evergreen browsers: + +- Chrome/Edge 90+ +- Firefox 88+ +- Safari 14+ + +Specifically relies on: + +- CSS custom properties (CSS variables) +- CSS Grid and Flexbox +- `:has()` selector (for sidenote positioning) +- `prefers-color-scheme` media query + +For older browsers, content remains readable but may lack advanced layout features like margin sidenotes. + +## Accessibility + +- All color combinations meet WCAG AA standards (4.5:1 for normal text, 3:1 for large text) +- Clear, visible focus states for keyboard navigation +- Encourages proper heading hierarchy, landmark regions, and form labels +- Works on all devices and respects user font size preferences +- No animations that could trigger vestibular disorders + +## Size & Performance + +The complete stylesheet is approximately 15KB uncompressed, 3-4KB when gzipped + +For maximum performance: + +1. Serve with proper compression (gzip or brotli) +2. Set appropriate cache headers +3. Consider inlining in `
-

Loading...

+
+

Loading...

+

A reactive framework demo powered by Volt.js

+
-
-

Event Bindings & Computed Values

-

- Count: 0
- Doubled: 0 -

- - - - -
+
+
+

Event Bindings & Computed Values

+

+ Count: 0
+ Doubled: 0 +

+ + + + +
-
-

Form Input

- -

You typed: nothing yet

-
+
+

Form Input

+ +

You typed: nothing yet

+
-
-

Class Bindings

-

This text has dynamic classes applied.

-

- Active: false -

- -
+
+

Class Bindings

+

This text has dynamic classes applied.

+

+ Active: false +

+ +
-
-

HTML Binding

-
Fallback content
-
+
+

HTML Binding

+
Fallback content
+
+
diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..3d08d89 --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,1070 @@ +/** + * Volt CSS - Classless stylesheet for elegant, readable web documents + * + * Design Philosophy: + * - Classless: Style semantic HTML5 elements directly + * - Dual theme: Automatic light/dark mode via prefers-color-scheme + * - Typography-first: Optimized for reading and information density + * - Accessible: WCAG AA contrast ratios, keyboard navigation support + * - Responsive: Mobile-first, adapts gracefully to all screen sizes + * + * Inspired by: magick.css, latex-css, sakura, matcha, mvp.css + */ + +/* ========================================================================== + CSS Custom Properties - Design Tokens + ========================================================================== */ + +/** + * Root-level CSS variables define the design system. + * Light theme is default, dark theme overrides via media query. + */ +:root { + /* Typography Scale - Modular scale based on 1.25 ratio */ + --font-size-base: 18px; + --font-size-sm: 0.889rem; /* 16px */ + --font-size-lg: 1.125rem; /* 20.25px */ + --font-size-xl: 1.266rem; /* 22.8px */ + --font-size-2xl: 1.424rem; /* 25.6px */ + --font-size-3xl: 1.802rem; /* 32.4px */ + --font-size-4xl: 2.027rem; /* 36.5px */ + --font-size-5xl: 2.566rem; /* 46.2px */ + + /* Font Families - Sans-serif with personality */ + /* System fonts for performance, fallback to serif for character */ + --font-sans: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-serif: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif; + --font-mono: "SF Mono", "Cascadia Code", "Fira Code", "Roboto Mono", Consolas, monospace; + + /* Spacing Scale - Based on 0.5rem increments */ + --space-xs: 0.25rem; /* 4px */ + --space-sm: 0.5rem; /* 8px */ + --space-md: 1rem; /* 16px */ + --space-lg: 1.5rem; /* 24px */ + --space-xl: 2rem; /* 32px */ + --space-2xl: 3rem; /* 48px */ + --space-3xl: 4rem; /* 64px */ + + /* Line Heights - Optimized for readability */ + --line-height-tight: 1.25; + --line-height-base: 1.6; + --line-height-relaxed: 1.8; + + /* Layout Dimensions */ + --content-width: 70ch; /* Optimal line length for reading */ + --sidenote-width: 18rem; /* Width of margin notes */ + --sidenote-gap: 2rem; /* Space between content and sidenotes */ + + /* Light Theme Colors - Duotone palette for clarity */ + --color-bg: #fefefe; + --color-bg-alt: #f5f5f5; /* For code blocks, tables */ + --color-text: #1a1a1a; + --color-text-muted: #666666; + --color-accent: #0066cc; /* Links, primary actions */ + --color-accent-hover: #0052a3; + --color-border: #d4d4d4; + --color-code-bg: #f8f8f8; + --color-mark: #fff3cd; /* Highlighted text */ + --color-success: #22863a; + --color-warning: #bf8700; + --color-error: #cb2431; + + /* Shadows - Subtle depth */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + + /* Border Radius */ + --radius-sm: 3px; + --radius-md: 6px; + --radius-lg: 8px; + + /* Transitions */ + --transition-fast: 150ms ease-in-out; + --transition-base: 250ms ease-in-out; +} + +/** + * Dark Theme Overrides + * Automatically applied when user prefers dark color scheme + */ +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #1a1a1a; + --color-bg-alt: #2a2a2a; + --color-text: #e6e6e6; + --color-text-muted: #a0a0a0; + --color-accent: #4da6ff; + --color-accent-hover: #66b3ff; + --color-border: #404040; + --color-code-bg: #2a2a2a; + --color-mark: #4a4a00; + --color-success: #34d058; + --color-warning: #ffdf5d; + --color-error: #f97583; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5); + } +} + +/* ========================================================================== + CSS Reset & Base Styles + ========================================================================== */ + +/** + * Modern CSS reset with sensible defaults + */ +*, *::before, *::after { + box-sizing: border-box; +} + +* { + margin: 0; + padding: 0; +} + +/** + * Document root configuration + * Sets base font size for rem calculations + */ +html { + font-size: var(--font-size-base); + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +/** + * Body element - Primary container + * Sets default typography and colors for the entire document + */ +body { + font-family: var(--font-sans); + font-size: 1rem; + line-height: var(--line-height-base); + color: var(--color-text); + background-color: var(--color-bg); + + /* Center content with optimal reading width */ + max-width: calc(var(--content-width) + var(--sidenote-width) + var(--sidenote-gap) * 2); + margin: 0 auto; + padding: var(--space-2xl) var(--space-lg); +} + +/* ========================================================================== + Typography - Hierarchy & Rhythm + ========================================================================== */ + +/** + * Headings hierarchy + * Uses modular scale for harmonious sizing + * Tighter line-height for larger text improves readability + */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: var(--line-height-tight); + color: var(--color-text); + margin-top: var(--space-2xl); + margin-bottom: var(--space-md); + letter-spacing: -0.02em; /* Slight negative tracking for display text */ +} + +/** + * Individual heading sizes + * h1-h3 use slightly larger weights for emphasis + */ +h1 { + font-size: var(--font-size-5xl); + margin-top: 0; /* No top margin on first heading */ +} + +h2 { + font-size: var(--font-size-4xl); +} + +h3 { + font-size: var(--font-size-3xl); +} + +h4 { + font-size: var(--font-size-2xl); +} + +h5 { + font-size: var(--font-size-xl); +} + +h6 { + font-size: var(--font-size-lg); + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/** + * Paragraph spacing + * Generous spacing between paragraphs aids scanning + */ +p { + margin-bottom: var(--space-lg); + max-width: var(--content-width); +} + +/** + * First paragraph after headings - No top margin + * Common convention in academic typography + */ +h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p { + margin-top: 0; +} + +/** + * Links - Accessible and distinctive + * Uses accent color with underline for clarity + */ +a { + color: var(--color-accent); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--color-accent-hover); +} + +a:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + border-radius: var(--radius-sm); +} + +/** + * Emphasis and strong elements + */ +em { + font-style: italic; +} + +strong { + font-weight: 700; +} + +/** + * Marked/highlighted text + */ +mark { + background-color: var(--color-mark); + padding: 0.1em 0.2em; + border-radius: var(--radius-sm); +} + +/** + * Subscript and superscript + * Prevents them from affecting line height + */ +sub, sup { + font-size: 0.75em; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/** + * Small text + * Also used for Tufte-style sidenotes (see sidenotes section) + */ +small { + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +/* ========================================================================== + Lists - Ordered & Unordered + ========================================================================== */ + +/** + * List spacing and indentation + * Nested lists inherit proper spacing + */ +ul, ol { + margin-bottom: var(--space-lg); + padding-left: var(--space-xl); + max-width: var(--content-width); +} + +/** + * List items + */ +li { + margin-bottom: var(--space-sm); +} + +li::marker { + color: var(--color-accent); +} + +/** + * Nested lists - Reduced spacing + */ +li > ul, li > ol { + margin-top: var(--space-sm); + margin-bottom: var(--space-sm); +} + +/** + * Description lists - For key-value pairs + */ +dl { + margin-bottom: var(--space-lg); + max-width: var(--content-width); +} + +dt { + font-weight: 700; + margin-top: var(--space-md); + margin-bottom: var(--space-xs); +} + +dd { + margin-left: var(--space-xl); + margin-bottom: var(--space-sm); + color: var(--color-text-muted); +} + +/* ========================================================================== + Tufte-Style Sidenotes + ========================================================================== */ + +/** + * Sidenotes using elements + * On desktop: positioned in right margin + * On mobile: inline with reduced emphasis + * + * Usage: Place inside

where you want the note to appear + * Example:

Main text here This appears in margin more text.

+ */ +@media (min-width: 768px) { + /** + * Parent paragraph must be positioned for absolute children + */ + p:has(small) { + position: relative; + } + + /** + * Pull small elements into the right margin + * Creates classic Tufte-style sidenote layout + */ + p small { + position: absolute; + left: calc(100% + var(--sidenote-gap)); + width: var(--sidenote-width); + font-size: 0.85rem; + line-height: var(--line-height-base); + margin-top: 0; + padding: var(--space-sm); + background-color: var(--color-bg-alt); + border-left: 2px solid var(--color-accent); + border-radius: var(--radius-sm); + } +} + +/** + * Mobile sidenotes - Inline with subtle styling + */ +@media (max-width: 767px) { + p small { + display: block; + margin-top: var(--space-sm); + margin-bottom: var(--space-sm); + padding: var(--space-sm); + background-color: var(--color-bg-alt); + border-left: 2px solid var(--color-accent); + border-radius: var(--radius-sm); + font-size: 0.9rem; + } +} + +/* ========================================================================== + Blockquotes & Citations + ========================================================================== */ + +/** + * Blockquote styling + * Left border for visual distinction, italic for emphasis + */ +blockquote { + margin: var(--space-xl) 0; + padding: var(--space-md) var(--space-lg); + border-left: 4px solid var(--color-accent); + background-color: var(--color-bg-alt); + font-style: italic; + color: var(--color-text-muted); + max-width: var(--content-width); + border-radius: var(--radius-sm); +} + +blockquote p:last-child { + margin-bottom: 0; +} + +/** + * Citation element + */ +cite { + font-style: normal; + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +blockquote cite::before { + content: " "; +} + +/* ========================================================================== + Code & Preformatted Text + ========================================================================== */ + +/** + * Inline code + * Monospace font with subtle background for distinction + */ +code { + font-family: var(--font-mono); + font-size: 0.9em; + padding: 0.15em 0.4em; + background-color: var(--color-code-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); +} + +/** + * Keyboard input + * Styled like keys on a keyboard + */ +kbd { + font-family: var(--font-mono); + font-size: 0.9em; + padding: 0.15em 0.4em; + background-color: var(--color-bg-alt); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + box-shadow: 0 1px 0 var(--color-border), 0 0 0 2px var(--color-bg) inset; +} + +/** + * Sample output + */ +samp { + font-family: var(--font-mono); + font-size: 0.9em; +} + +/** + * Variable + */ +var { + font-family: var(--font-mono); + font-style: normal; + font-weight: 600; +} + +/** + * Preformatted code blocks + * Horizontal scrolling for overflow, no word wrap + */ +pre { + margin: var(--space-xl) 0; + padding: var(--space-lg); + background-color: var(--color-code-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow-x: auto; + max-width: var(--content-width); + line-height: var(--line-height-base); +} + +pre code { + padding: 0; + background: none; + border: none; + font-size: 0.875rem; +} + +/* ========================================================================== + Horizontal Rules + ========================================================================== */ + +/** + * Section dividers + * Centered decorative element with breathing room + */ +hr { + margin: var(--space-3xl) auto; + border: none; + border-top: 1px solid var(--color-border); + max-width: 50%; +} + +/* ========================================================================== + Tables + ========================================================================== */ + +/** + * Table container for horizontal scrolling on small screens + */ +table { + width: 100%; + max-width: var(--content-width); + margin: var(--space-xl) 0; + border-collapse: collapse; + overflow-x: auto; + display: block; +} + +/** + * Table header styling + * Bold text with bottom border for separation + */ +thead { + background-color: var(--color-bg-alt); + border-bottom: 2px solid var(--color-border); +} + +th { + padding: var(--space-sm) var(--space-md); + text-align: left; + font-weight: 700; + color: var(--color-text); +} + +/** + * Table cells + */ +td { + padding: var(--space-sm) var(--space-md); + border-bottom: 1px solid var(--color-border); +} + +/** + * Zebra striping for easier row scanning + */ +tbody tr:nth-child(even) { + background-color: var(--color-bg-alt); +} + +/** + * Hover state for interactive tables + */ +tbody tr:hover { + background-color: var(--color-border); + transition: background-color var(--transition-fast); +} + +/* ========================================================================== + Forms & Input Elements + ========================================================================== */ + +/** + * Form container spacing + */ +form { + margin: var(--space-xl) 0; + max-width: var(--content-width); +} + +/** + * Fieldset grouping + */ +fieldset { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-lg); + margin-bottom: var(--space-lg); +} + +legend { + font-weight: 700; + padding: 0 var(--space-sm); + color: var(--color-text); +} + +/** + * Labels + * Block display for better touch targets + */ +label { + display: block; + margin-bottom: var(--space-xs); + font-weight: 600; + color: var(--color-text); +} + +/** + * Required field indicator + */ +label:has(+ input[required])::after, +label:has(+ textarea[required])::after, +label:has(+ select[required])::after { + content: " *"; + color: var(--color-error); +} + +/** + * Text inputs and textareas + * Consistent sizing and interaction states + */ +input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]), +select, +textarea { + width: 100%; + padding: var(--space-sm) var(--space-md); + font-family: inherit; + font-size: 1rem; + line-height: var(--line-height-base); + color: var(--color-text); + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + margin-bottom: var(--space-md); +} + +/** + * Focus states for inputs + * Clear visual feedback for keyboard navigation + */ +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); +} + +/** + * Disabled state + */ +input:disabled, +select:disabled, +textarea:disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--color-bg-alt); +} + +/** + * Textarea specific + */ +textarea { + resize: vertical; + min-height: 8rem; +} + +/** + * Checkboxes and radio buttons + */ +input[type="checkbox"], +input[type="radio"] { + margin-right: var(--space-xs); + accent-color: var(--color-accent); +} + +/** + * File input + */ +input[type="file"] { + padding: var(--space-sm); + border: 1px dashed var(--color-border); + border-radius: var(--radius-sm); + cursor: pointer; + margin-bottom: var(--space-md); +} + +/** + * Range input + */ +input[type="range"] { + width: 100%; + margin: var(--space-md) 0; + accent-color: var(--color-accent); +} + +/** + * Progress and meter + */ +progress, meter { + width: 100%; + height: 1.5rem; + margin: var(--space-md) 0; + border-radius: var(--radius-sm); + overflow: hidden; +} + +/* ========================================================================== + Buttons + ========================================================================== */ + +/** + * Button styling + * Primary action style with hover and active states + */ +button, +input[type="submit"], +input[type="button"], +input[type="reset"] { + display: inline-block; + padding: var(--space-sm) var(--space-lg); + font-family: inherit; + font-size: 1rem; + font-weight: 600; + line-height: 1; + color: white; + background-color: var(--color-accent); + border: none; + border-radius: var(--radius-md); + cursor: pointer; + text-decoration: none; + transition: background-color var(--transition-fast), transform var(--transition-fast); + margin-right: var(--space-sm); + margin-bottom: var(--space-sm); +} + +button:hover, +input[type="submit"]:hover, +input[type="button"]:hover { + background-color: var(--color-accent-hover); +} + +button:active, +input[type="submit"]:active, +input[type="button"]:active { + transform: translateY(1px); +} + +/** + * Reset button - Subdued styling + */ +input[type="reset"] { + background-color: var(--color-bg-alt); + color: var(--color-text); + border: 1px solid var(--color-border); +} + +input[type="reset"]:hover { + background-color: var(--color-border); +} + +/** + * Disabled buttons + */ +button:disabled, +input[type="submit"]:disabled, +input[type="button"]:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/** + * Focus state for buttons + */ +button:focus-visible, +input[type="submit"]:focus-visible, +input[type="button"]:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +/* ========================================================================== + Media Elements + ========================================================================== */ + +/** + * Images + * Responsive by default, maintains aspect ratio + */ +img { + max-width: 100%; + height: auto; + display: block; + border-radius: var(--radius-sm); +} + +/** + * Figures with captions + * Common in academic and technical writing + */ +figure { + margin: var(--space-xl) 0; + max-width: var(--content-width); +} + +figcaption { + margin-top: var(--space-sm); + font-size: var(--font-size-sm); + color: var(--color-text-muted); + font-style: italic; + text-align: center; +} + +/** + * Video and audio + * Responsive and accessible + */ +video, audio { + max-width: 100%; + margin: var(--space-xl) 0; +} + +/** + * Canvas and SVG + */ +canvas, svg { + max-width: 100%; + height: auto; +} + +/* ========================================================================== + Embedded Content + ========================================================================== */ + +/** + * iframe - Responsive wrapper + */ +iframe { + max-width: 100%; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + margin: var(--space-xl) 0; +} + +/* ========================================================================== + Semantic HTML5 Elements + ========================================================================== */ + +/** + * Article and Section + * Spacing between major content blocks + */ +article, section { + margin-bottom: var(--space-3xl); +} + +/** + * Aside + * Complementary content, styled distinctly + */ +aside { + padding: var(--space-lg); + margin: var(--space-xl) 0; + background-color: var(--color-bg-alt); + border-left: 4px solid var(--color-accent); + border-radius: var(--radius-sm); + max-width: var(--content-width); +} + +/** + * Header and Footer + */ +header { + margin-bottom: var(--space-2xl); + padding-bottom: var(--space-xl); + border-bottom: 1px solid var(--color-border); +} + +footer { + margin-top: var(--space-3xl); + padding-top: var(--space-xl); + border-top: 1px solid var(--color-border); + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +/** + * Nav + * Navigation menus + */ +nav { + margin: var(--space-lg) 0; +} + +nav ul { + list-style: none; + padding: 0; + display: flex; + gap: var(--space-md); + flex-wrap: wrap; +} + +nav li { + margin: 0; +} + +/** + * Details and Summary + * Disclosure widget for expandable content + */ +details { + margin: var(--space-lg) 0; + padding: var(--space-md); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + max-width: var(--content-width); +} + +summary { + font-weight: 700; + cursor: pointer; + user-select: none; + padding: var(--space-sm); + margin: calc(-1 * var(--space-sm)); + transition: background-color var(--transition-fast); +} + +summary:hover { + background-color: var(--color-bg-alt); +} + +details[open] summary { + margin-bottom: var(--space-md); + border-bottom: 1px solid var(--color-border); +} + +/* ========================================================================== + Utility Classes (Minimal, for framework integration) + ========================================================================== */ + +/** + * Screen reader only + * Hides content visually but keeps it accessible to assistive technology + */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* ========================================================================== + Print Styles + ========================================================================== */ + +/** + * Print-specific optimizations + */ +@media print { + body { + font-size: 12pt; + line-height: 1.5; + color: #000; + background: #fff; + } + + a { + text-decoration: underline; + color: #000; + } + + /* Display URLs after links */ + a[href^="http"]::after { + content: " (" attr(href) ")"; + font-size: 0.8em; + } + + /* Hide sidenotes positioning, show inline */ + @media (min-width: 768px) { + p small { + position: static; + width: auto; + left: auto; + } + } + + /* Avoid page breaks inside elements */ + h1, h2, h3, h4, h5, h6, p, li, blockquote { + page-break-inside: avoid; + } + + /* Ensure images fit page */ + img { + max-width: 100% !important; + } +} + +/* ========================================================================== + Responsive Breakpoints + ========================================================================== */ + +/** + * Tablet and below - Reduce spacing + */ +@media (max-width: 768px) { + :root { + --font-size-base: 16px; + --space-2xl: 2rem; + --space-3xl: 3rem; + } + + body { + padding: var(--space-lg) var(--space-md); + } + + h1 { + font-size: var(--font-size-4xl); + } + + h2 { + font-size: var(--font-size-3xl); + } + + /* Stack navigation vertically */ + nav ul { + flex-direction: column; + gap: var(--space-sm); + } +} + +/** + * Mobile - Further reduced spacing and sizing + */ +@media (max-width: 480px) { + :root { + --font-size-base: 15px; + } + + body { + padding: var(--space-md) var(--space-sm); + } + + h1 { + font-size: var(--font-size-3xl); + } + + /* Reduce horizontal padding on smaller screens */ + pre { + padding: var(--space-md); + } + + table { + font-size: var(--font-size-sm); + } +} -- 2.51.2