a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(wip): prototyping classless css add-on

+2338 -166
+409
cli/src/commands/css-docs.ts
··· 1 + import { mkdir, readFile, writeFile } from "node:fs/promises"; 2 + import path from "node:path"; 3 + import { echo } from "../console/echo"; 4 + 5 + type CSSComment = { selector: string; comment: string }; 6 + type CSSVariable = { name: string; value: string; category: string }; 7 + type ElementCoverage = { element: string; covered: boolean }; 8 + 9 + /** 10 + * Extract CSS doc comments from CSS file 11 + * Parses block comments and associates them with selectors 12 + */ 13 + function extractCSSComments(cssContent: string): CSSComment[] { 14 + const comments: CSSComment[] = []; 15 + const lines = cssContent.split("\n"); 16 + 17 + let currentComment = ""; 18 + let inComment = false; 19 + let commentLines: string[] = []; 20 + 21 + for (let i = 0; i < lines.length; i++) { 22 + const line = lines[i]; 23 + const trimmed = line.trim(); 24 + 25 + if (trimmed.startsWith("/**") || trimmed.startsWith("/*")) { 26 + inComment = true; 27 + commentLines = []; 28 + const commentText = trimmed.replace(/^\/\*+\s*/, "").replace(/\*\/\s*$/, ""); 29 + if (commentText && !commentText.startsWith("=")) { 30 + commentLines.push(commentText); 31 + } 32 + continue; 33 + } 34 + 35 + if (inComment) { 36 + if (trimmed.includes("*/")) { 37 + const commentText = trimmed.replace(/\*\/.*$/, "").replace(/^\*\s*/, ""); 38 + if (commentText && !commentText.startsWith("=")) { 39 + commentLines.push(commentText); 40 + } 41 + currentComment = commentLines.join(" ").trim(); 42 + inComment = false; 43 + 44 + for (let j = i + 1; j < lines.length; j++) { 45 + const nextLine = lines[j].trim(); 46 + if (nextLine === "" || nextLine.startsWith("/*")) { 47 + continue; 48 + } 49 + 50 + if (nextLine.includes("{") || j + 1 < lines.length && lines[j + 1].includes("{")) { 51 + const selector = nextLine.replace("{", "").trim(); 52 + if (selector && currentComment) { 53 + comments.push({ selector, comment: currentComment }); 54 + } 55 + break; 56 + } 57 + break; 58 + } 59 + currentComment = ""; 60 + continue; 61 + } 62 + 63 + const commentText = trimmed.replace(/^\*\s*/, ""); 64 + if (commentText && !commentText.startsWith("=")) { 65 + commentLines.push(commentText); 66 + } 67 + } 68 + } 69 + 70 + return comments; 71 + } 72 + 73 + /** 74 + * Extract CSS custom properties (variables) from :root 75 + * Groups them by category based on naming conventions 76 + */ 77 + function extractCSSVariables(cssContent: string): CSSVariable[] { 78 + const variables: CSSVariable[] = []; 79 + const lines = cssContent.split("\n"); 80 + let inRoot = false; 81 + 82 + for (const line of lines) { 83 + const trimmed = line.trim(); 84 + 85 + if (trimmed.startsWith(":root")) { 86 + inRoot = true; 87 + continue; 88 + } 89 + 90 + if (inRoot && trimmed === "}") { 91 + inRoot = false; 92 + continue; 93 + } 94 + 95 + if (inRoot && trimmed.startsWith("--")) { 96 + const match = trimmed.match(/^(--[a-z0-9-]+)\s*:\s*([^;]+);/); 97 + if (match) { 98 + const [, name, value] = match; 99 + const category = categorizeCSSVariable(name); 100 + variables.push({ name, value: value.trim(), category }); 101 + } 102 + } 103 + } 104 + 105 + return variables; 106 + } 107 + 108 + /** 109 + * Categorize CSS variable by name prefix 110 + */ 111 + function categorizeCSSVariable(name: string): string { 112 + if (name.startsWith("--font")) return "Typography"; 113 + if (name.startsWith("--line-height")) return "Typography"; 114 + if (name.startsWith("--space")) return "Spacing"; 115 + if (name.startsWith("--color")) return "Colors"; 116 + if (name.startsWith("--shadow")) return "Effects"; 117 + if (name.startsWith("--radius")) return "Effects"; 118 + if (name.startsWith("--transition")) return "Effects"; 119 + if (name.startsWith("--content")) return "Layout"; 120 + if (name.startsWith("--sidenote")) return "Layout"; 121 + return "Other"; 122 + } 123 + 124 + /** 125 + * Validate CSS element coverage 126 + * Checks which HTML elements have styling defined 127 + */ 128 + function validateElementCoverage(cssContent: string): ElementCoverage[] { 129 + const elementsToCheck = [ 130 + "html", 131 + "body", 132 + "h1", 133 + "h2", 134 + "h3", 135 + "h4", 136 + "h5", 137 + "h6", 138 + "p", 139 + "a", 140 + "em", 141 + "strong", 142 + "mark", 143 + "small", 144 + "sub", 145 + "sup", 146 + "ul", 147 + "ol", 148 + "li", 149 + "dl", 150 + "dt", 151 + "dd", 152 + "blockquote", 153 + "cite", 154 + "code", 155 + "pre", 156 + "kbd", 157 + "samp", 158 + "var", 159 + "hr", 160 + "table", 161 + "thead", 162 + "tbody", 163 + "th", 164 + "td", 165 + "tr", 166 + "form", 167 + "fieldset", 168 + "legend", 169 + "label", 170 + "input", 171 + "select", 172 + "textarea", 173 + "button", 174 + "img", 175 + "figure", 176 + "figcaption", 177 + "video", 178 + "audio", 179 + "canvas", 180 + "svg", 181 + "iframe", 182 + "article", 183 + "section", 184 + "aside", 185 + "header", 186 + "footer", 187 + "nav", 188 + "details", 189 + "summary", 190 + ]; 191 + 192 + const coverage: ElementCoverage[] = []; 193 + 194 + for (const element of elementsToCheck) { 195 + const patterns = [ 196 + // element { 197 + new RegExp(`^${element}\\s*\\{`, "m"), 198 + // element, 199 + new RegExp(`^${element},`, "m"), 200 + // , element { 201 + new RegExp(`,\\s*${element}\\s*\\{`, "m"), 202 + // element:pseudo 203 + new RegExp(`^${element}:`, "m"), 204 + // element[attr] 205 + new RegExp(`${element}\\[`, "m"), 206 + ]; 207 + 208 + const covered = patterns.some((pattern) => pattern.test(cssContent)); 209 + coverage.push({ element, covered }); 210 + } 211 + 212 + return coverage; 213 + } 214 + 215 + /** 216 + * Generate markdown documentation from extracted data 217 + */ 218 + function generateSemanticsDocs(comments: CSSComment[], variables: CSSVariable[], coverage: ElementCoverage[]): string { 219 + const lines: string[] = [ 220 + "# Volt CSS Semantics", 221 + "", 222 + "Auto-generated documentation from base.css", 223 + "", 224 + "## CSS Custom Properties", 225 + "", 226 + "All design tokens defined in the stylesheet.", 227 + "", 228 + ]; 229 + 230 + const categoryMap = new Map<string, CSSVariable[]>(); 231 + for (const variable of variables) { 232 + if (!categoryMap.has(variable.category)) { 233 + categoryMap.set(variable.category, []); 234 + } 235 + categoryMap.get(variable.category)!.push(variable); 236 + } 237 + 238 + for (const [category, vars] of categoryMap) { 239 + lines.push(`### ${category}`, ""); 240 + for (const v of vars) { 241 + lines.push(`- \`${v.name}\`: \`${v.value}\``); 242 + } 243 + lines.push(""); 244 + } 245 + 246 + lines.push("## Element Coverage", "", "HTML elements with defined styling in the stylesheet.", ""); 247 + 248 + const covered = coverage.filter((c) => c.covered); 249 + const notCovered = coverage.filter((c) => !c.covered); 250 + 251 + lines.push(`**Coverage**: ${covered.length}/${coverage.length} elements`, "", "### Styled Elements", ""); 252 + 253 + const coveredByCategory = groupElementsByCategory(covered.map((c) => c.element)); 254 + for (const [category, elements] of Object.entries(coveredByCategory)) { 255 + lines.push(`**${category}**: ${elements.join(", ")}`); 256 + } 257 + lines.push(""); 258 + 259 + if (notCovered.length > 0) { 260 + lines.push("### Unstyled Elements", "", notCovered.map((c) => c.element).join(", "), ""); 261 + } 262 + 263 + lines.push("## Documentation Comments", "", "Inline documentation extracted from CSS comments.", ""); 264 + 265 + for (const comment of comments) { 266 + if (comment.comment.length > 200) { 267 + continue; 268 + } 269 + 270 + lines.push(`### \`${comment.selector}\``, ""); 271 + lines.push(comment.comment, ""); 272 + } 273 + 274 + return lines.join("\n"); 275 + } 276 + 277 + /** 278 + * Group HTML elements by category for better organization 279 + */ 280 + function groupElementsByCategory(elements: string[]): Record<string, string[]> { 281 + const categories: Record<string, string[]> = { 282 + "Document Structure": [], 283 + "Typography": [], 284 + "Lists": [], 285 + "Semantic": [], 286 + "Forms": [], 287 + "Tables": [], 288 + "Media": [], 289 + "Code": [], 290 + }; 291 + 292 + const categoryMap: Record<string, string> = { 293 + html: "Document Structure", 294 + body: "Document Structure", 295 + h1: "Typography", 296 + h2: "Typography", 297 + h3: "Typography", 298 + h4: "Typography", 299 + h5: "Typography", 300 + h6: "Typography", 301 + p: "Typography", 302 + a: "Typography", 303 + em: "Typography", 304 + strong: "Typography", 305 + mark: "Typography", 306 + small: "Typography", 307 + sub: "Typography", 308 + sup: "Typography", 309 + hr: "Typography", 310 + ul: "Lists", 311 + ol: "Lists", 312 + li: "Lists", 313 + dl: "Lists", 314 + dt: "Lists", 315 + dd: "Lists", 316 + blockquote: "Semantic", 317 + cite: "Semantic", 318 + article: "Semantic", 319 + section: "Semantic", 320 + aside: "Semantic", 321 + header: "Semantic", 322 + footer: "Semantic", 323 + nav: "Semantic", 324 + details: "Semantic", 325 + summary: "Semantic", 326 + form: "Forms", 327 + fieldset: "Forms", 328 + legend: "Forms", 329 + label: "Forms", 330 + input: "Forms", 331 + select: "Forms", 332 + textarea: "Forms", 333 + button: "Forms", 334 + table: "Tables", 335 + thead: "Tables", 336 + tbody: "Tables", 337 + th: "Tables", 338 + td: "Tables", 339 + tr: "Tables", 340 + img: "Media", 341 + figure: "Media", 342 + figcaption: "Media", 343 + video: "Media", 344 + audio: "Media", 345 + canvas: "Media", 346 + svg: "Media", 347 + iframe: "Media", 348 + code: "Code", 349 + pre: "Code", 350 + kbd: "Code", 351 + samp: "Code", 352 + var: "Code", 353 + }; 354 + 355 + for (const element of elements) { 356 + const category = categoryMap[element] || "Other"; 357 + if (!categories[category]) { 358 + categories[category] = []; 359 + } 360 + categories[category].push(element); 361 + } 362 + 363 + return Object.fromEntries(Object.entries(categories).filter(([, els]) => els.length > 0)); 364 + } 365 + 366 + /** 367 + * CSS documentation command implementation 368 + * Generates semantics.md from base.css 369 + */ 370 + export async function cssDocsCommand(): Promise<void> { 371 + const projectRoot = path.join(process.cwd(), ".."); 372 + const cssPath = path.join(projectRoot, "src", "styles", "base.css"); 373 + const outputDir = path.join(projectRoot, "docs", "css"); 374 + const outputPath = path.join(outputDir, "semantics.md"); 375 + 376 + echo.title("\nGenerating CSS Documentation\n"); 377 + 378 + let cssContent: string; 379 + try { 380 + cssContent = await readFile(cssPath, "utf8"); 381 + echo.ok(`Read ${cssPath}`); 382 + } catch (error) { 383 + echo.err(`Failed to read CSS file: ${cssPath}`); 384 + throw error; 385 + } 386 + 387 + echo.info("\nExtracting CSS documentation..."); 388 + 389 + const comments = extractCSSComments(cssContent); 390 + echo.ok(` Found ${comments.length} documented selectors`); 391 + 392 + const variables = extractCSSVariables(cssContent); 393 + echo.ok(` Found ${variables.length} CSS custom properties`); 394 + 395 + const coverage = validateElementCoverage(cssContent); 396 + const coveredCount = coverage.filter((c) => c.covered).length; 397 + echo.ok(` Element coverage: ${coveredCount}/${coverage.length}`); 398 + 399 + const markdown = generateSemanticsDocs(comments, variables, coverage); 400 + 401 + await mkdir(outputDir, { recursive: true }); 402 + await writeFile(outputPath, markdown, "utf8"); 403 + 404 + echo.success(`\nCSS documentation generated: docs/css/semantics.md\n`); 405 + echo.label("Summary:"); 406 + echo.text(` CSS Comments: ${comments.length}`); 407 + echo.text(` CSS Variables: ${variables.length}`); 408 + echo.text(` Element Coverage: ${coveredCount}/${coverage.length}\n`); 409 + }
+78 -80
cli/src/commands/docs.ts
··· 1 - import chalk from "chalk"; 2 1 import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; 3 2 import path from "node:path"; 4 3 import ts from "typescript"; 4 + import { echo } from "../console/echo.js"; 5 5 6 6 type Member = { name: string; type: string; docs?: string }; 7 + 7 8 type EntryKind = "function" | "interface" | "type" | "class"; 8 9 9 10 type DocumentEntry = { ··· 20 21 /** 21 22 * Extract and parse JSDoc comment text 22 23 */ 23 - function extractJSDocument(node: ts.Node, sourceFile: ts.SourceFile): JSDocumentParsed { 24 + function extractJSDoc(node: ts.Node, sourceFile: ts.SourceFile): JSDocumentParsed { 24 25 const fullText = sourceFile.getFullText(); 25 26 const ranges = ts.getLeadingCommentRanges(fullText, node.getFullStart()); 26 27 ··· 79 80 /** 80 81 * Extract function signature 81 82 */ 82 - function extractFunctionSignature(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): string { 83 + function extractFnSig(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): string { 83 84 const start = node.getStart(sourceFile); 84 85 const end = node.body ? node.body.getStart(sourceFile) : node.getEnd(); 85 86 return sourceFile.text.substring(start, end).trim().replaceAll(/\s+/g, " "); ··· 88 89 /** 89 90 * Extract interface members 90 91 */ 91 - function extractInterfaceMembers( 92 - node: ts.InterfaceDeclaration, 93 - sourceFile: ts.SourceFile, 94 - ): Array<{ name: string; type: string; docs?: string }> { 95 - const members: Array<{ name: string; type: string; docs?: string }> = []; 92 + function extractIMembers(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): Array<Member> { 93 + const members: Array<Member> = []; 96 94 97 95 for (const member of node.members) { 98 96 if (ts.isPropertySignature(member) && member.name) { 99 97 const name = member.name.getText(sourceFile); 100 98 const type = member.type ? member.type.getText(sourceFile) : "unknown"; 101 - const { description } = extractJSDocument(member, sourceFile); 99 + const { description } = extractJSDoc(member, sourceFile); 102 100 103 101 members.push({ name, type, docs: description || undefined }); 104 102 } ··· 108 106 } 109 107 110 108 /** 111 - * Parse a TypeScript file and extract documentation 112 - */ 113 - function parseFile(filePath: string, content: string): DocumentEntry[] { 114 - const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); 115 - const entries: DocumentEntry[] = []; 116 - 117 - function visit(node: ts.Node) { 118 - if (ts.isFunctionDeclaration(node) && node.name) { 119 - const modifiers = node.modifiers; 120 - const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); 121 - 122 - if (isExported) { 123 - const name = node.name.text; 124 - const { description, examples } = extractJSDocument(node, sourceFile); 125 - const signature = extractFunctionSignature(node, sourceFile); 126 - 127 - entries.push({ name, kind: "function", description, examples, signature }); 128 - } 129 - } 130 - 131 - if (ts.isInterfaceDeclaration(node)) { 132 - const modifiers = node.modifiers; 133 - const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); 134 - 135 - if (isExported) { 136 - const name = node.name.text; 137 - const { description, examples } = extractJSDocument(node, sourceFile); 138 - const members = extractInterfaceMembers(node, sourceFile); 139 - 140 - entries.push({ name, kind: "interface", description, examples, members }); 141 - } 142 - } 143 - 144 - if (ts.isTypeAliasDeclaration(node)) { 145 - const modifiers = node.modifiers; 146 - const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); 147 - 148 - if (isExported) { 149 - const name = node.name.text; 150 - const { description, examples } = extractJSDocument(node, sourceFile); 151 - const signature = node.type.getText(sourceFile); 152 - 153 - entries.push({ name, kind: "type", description, examples, signature }); 154 - } 155 - } 156 - 157 - ts.forEachChild(node, visit); 158 - } 159 - 160 - visit(sourceFile); 161 - return entries; 162 - } 163 - 164 - /** 165 109 * Generate markdown for a documentation entry 166 110 */ 167 - function generateMarkdown(entries: DocumentEntry[], moduleName: string, moduleDocs: string): string { 111 + function generateMD(entries: DocumentEntry[], moduleName: string, moduleDocs: string): string { 168 112 const lines: string[] = []; 169 113 170 114 lines.push(`# ${moduleName}`, ""); ··· 210 154 /** 211 155 * Extract module-level documentation 212 156 */ 213 - function extractModuleDocs(content: string): string { 157 + function extractModDocs(content: string): string { 214 158 const lines = content.split("\n"); 215 - const documentLines: string[] = []; 216 - let inDocument = false; 159 + const docLines: string[] = []; 160 + let inDoc = false; 217 161 218 162 for (const line of lines) { 219 163 const trimmed = line.trim(); 220 164 221 165 if (trimmed === "/**") { 222 - inDocument = true; 166 + inDoc = true; 223 167 continue; 224 168 } 225 169 226 - if (inDocument) { 170 + if (inDoc) { 227 171 if (trimmed === "*/") { 228 172 break; 229 173 } 230 174 231 175 const cleaned = trimmed.replace(/^\*\s?/, ""); 232 176 if (!cleaned.startsWith("@packageDocumentation")) { 233 - documentLines.push(cleaned); 177 + docLines.push(cleaned); 178 + } 179 + } 180 + } 181 + 182 + return docLines.join("\n").trim(); 183 + } 184 + 185 + /** 186 + * Parse a TypeScript file and extract documentation 187 + */ 188 + function parseFile(filePath: string, content: string): DocumentEntry[] { 189 + const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); 190 + const entries: DocumentEntry[] = []; 191 + 192 + function visit(node: ts.Node) { 193 + if (ts.isFunctionDeclaration(node) && node.name) { 194 + const modifiers = node.modifiers; 195 + const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); 196 + 197 + if (isExported) { 198 + const name = node.name.text; 199 + const { description, examples } = extractJSDoc(node, sourceFile); 200 + const signature = extractFnSig(node, sourceFile); 201 + 202 + entries.push({ name, kind: "function", description, examples, signature }); 203 + } 204 + } 205 + 206 + if (ts.isInterfaceDeclaration(node)) { 207 + const modifiers = node.modifiers; 208 + const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); 209 + 210 + if (isExported) { 211 + const name = node.name.text; 212 + const { description, examples } = extractJSDoc(node, sourceFile); 213 + const members = extractIMembers(node, sourceFile); 214 + 215 + entries.push({ name, kind: "interface", description, examples, members }); 234 216 } 235 217 } 218 + 219 + if (ts.isTypeAliasDeclaration(node)) { 220 + const modifiers = node.modifiers; 221 + const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); 222 + 223 + if (isExported) { 224 + const name = node.name.text; 225 + const { description, examples } = extractJSDoc(node, sourceFile); 226 + const signature = node.type.getText(sourceFile); 227 + 228 + entries.push({ name, kind: "type", description, examples, signature }); 229 + } 230 + } 231 + 232 + ts.forEachChild(node, visit); 236 233 } 237 234 238 - return documentLines.join("\n").trim(); 235 + visit(sourceFile); 236 + return entries; 239 237 } 240 238 241 239 /** ··· 249 247 return; 250 248 } 251 249 252 - const moduleDocs = extractModuleDocs(content); 250 + const moduleDocs = extractModDocs(content); 253 251 const relativePath = path.relative(baseDir, filePath); 254 252 const moduleName = path.basename(relativePath, ".ts"); 255 - const markdown = generateMarkdown(entries, moduleName, moduleDocs); 253 + const markdown = generateMD(entries, moduleName, moduleDocs); 256 254 257 255 const outputPath = path.join(outputDir, `${moduleName}.md`); 258 256 await writeFile(outputPath, markdown, "utf8"); 259 257 260 - console.log(chalk.green(` Generated: ${relativePath} -> api/${moduleName}.md`)); 258 + echo.ok(` Generated: ${relativePath} -> api/${moduleName}.md`); 261 259 } 262 260 263 261 /** ··· 283 281 * Docs command implementation 284 282 */ 285 283 export async function docsCommand(): Promise<void> { 286 - const projectRoot = path.join(process.cwd(), ".."); 287 - const srcDir = path.join(projectRoot, "src"); 288 - const docsDir = path.join(projectRoot, "docs", "api"); 284 + const root = path.join(process.cwd(), ".."); 285 + const srcDir = path.join(root, "src"); 286 + const docsDir = path.join(root, "docs", "api"); 289 287 290 - console.log(chalk.blue.bold("\nGenerating API Documentation\n")); 288 + echo.title("\nGenerating API Documentation\n"); 291 289 292 290 await mkdir(docsDir, { recursive: true }); 293 291 294 292 const files = await findTsFiles(srcDir); 295 293 296 - console.log(chalk.cyan(`Found ${files.length} TypeScript files\n`)); 294 + echo.info(`Found ${files.length} TypeScript files\n`); 297 295 298 296 for (const file of files) { 299 297 await processFile(file, srcDir, docsDir); 300 298 } 301 299 302 - console.log(chalk.green.bold(`\nAPI documentation generated in docs/api/\n`)); 300 + echo.success(`\nAPI documentation generated in docs/api/\n`); 303 301 }
+20 -20
cli/src/commands/stats.ts
··· 1 - import chalk from "chalk"; 2 1 import { readdir, readFile, stat } from "node:fs/promises"; 3 2 import path from "node:path"; 3 + import { echo } from "../console/echo.js"; 4 4 5 5 type FileStats = { path: string; lines: number; totalLines: number }; 6 6 type DirectoryStats = { totalLines: number; codeLines: number; files: FileStats[] }; ··· 93 93 const srcDir = path.join(projectRoot, "src"); 94 94 const testDir = path.join(projectRoot, "test"); 95 95 96 - console.log(chalk.blue.bold("\nVolt.js Code Statistics\n")); 96 + echo.title("\nVolt.js Code Statistics\n"); 97 97 98 98 const srcStats = await collectStats(srcDir, projectRoot); 99 99 100 - console.log(chalk.cyan("Source Code (src/):")); 101 - console.log(` Files: ${srcStats.files.length}`); 102 - console.log(` Total Lines: ${srcStats.totalLines}`); 103 - console.log(chalk.green(` Code Lines: ${srcStats.codeLines}`)); 104 - console.log(` Doc/Comments: ${srcStats.totalLines - srcStats.codeLines}`); 100 + echo.label("Source Code (src/):"); 101 + echo.text(` Files: ${srcStats.files.length}`); 102 + echo.text(` Total Lines: ${srcStats.totalLines}`); 103 + echo.ok(` Code Lines: ${srcStats.codeLines}`); 104 + echo.text(` Doc/Comments: ${srcStats.totalLines - srcStats.codeLines}`); 105 105 106 106 let totalCode = srcStats.codeLines; 107 107 let totalTotal = srcStats.totalLines; ··· 111 111 if (includeFull) { 112 112 const testStats = await collectStats(testDir, projectRoot); 113 113 114 - console.log(chalk.cyan("\nTest Code (test/):")); 115 - console.log(` Files: ${testStats.files.length}`); 116 - console.log(` Total Lines: ${testStats.totalLines}`); 117 - console.log(chalk.green(` Code Lines: ${testStats.codeLines}`)); 118 - console.log(` Doc/Comments: ${testStats.totalLines - testStats.codeLines}`); 114 + echo.label("\nTest Code (test/):"); 115 + echo.text(` Files: ${testStats.files.length}`); 116 + echo.text(` Total Lines: ${testStats.totalLines}`); 117 + echo.ok(` Code Lines: ${testStats.codeLines}`); 118 + echo.text(` Doc/Comments: ${testStats.totalLines - testStats.codeLines}`); 119 119 120 120 totalCode += testStats.codeLines; 121 121 totalTotal += testStats.totalLines; 122 122 totalFileCount += testStats.files.length; 123 123 } 124 124 125 - console.log(chalk.blue.bold("\nTotal:")); 126 - console.log(` Files: ${totalFileCount}`); 127 - console.log(` Total Lines: ${totalTotal}`); 128 - console.log(chalk.green.bold(` Code Lines: ${totalCode}`)); 129 - console.log(` Doc/Comments: ${totalTotal - totalCode}`); 125 + echo.title("\nTotal:"); 126 + echo.text(` Files: ${totalFileCount}`); 127 + echo.text(` Total Lines: ${totalTotal}`); 128 + echo.success(` Code Lines: ${totalCode}`); 129 + echo.text(` Doc/Comments: ${totalTotal - totalCode}`); 130 130 131 131 if (process.env.VERBOSE) { 132 - console.log(chalk.yellow("\n\nFile Breakdown:")); 132 + echo.warn("\n\nFile Breakdown:"); 133 133 for (const file of srcStats.files) { 134 - console.log(` ${file.path}: ${file.lines} lines`); 134 + echo.text(` ${file.path}: ${file.lines} lines`); 135 135 } 136 136 } 137 137 138 - console.log(); 138 + echo.text(); 139 139 }
+40
cli/src/console/echo.ts
··· 1 + import chalk from "chalk"; 2 + 3 + type Echo = Record< 4 + "info" | "success" | "ok" | "warn" | "text" | "err" | "danger" | "label" | "title", 5 + (message?: any, ...optionalParams: any[]) => void 6 + >; 7 + 8 + export const echo: Echo = { 9 + /** 10 + * Red text to stderr 11 + */ 12 + err(message, ...optionalParams) { 13 + console.error(chalk.red(message), ...optionalParams); 14 + }, 15 + /** 16 + * Red text for recoverable errors (to stdout) 17 + */ 18 + danger(message, ...optionalParams) { 19 + console.log(chalk.red(message), ...optionalParams); 20 + }, 21 + ok(message, ...optionalParams) { 22 + console.log(chalk.green(message), ...optionalParams); 23 + }, 24 + success(message, ...optionalParams) { 25 + console.log(chalk.green.bold(message), ...optionalParams); 26 + }, 27 + info(message, ...optionalParams) { 28 + console.log(chalk.cyan(message), ...optionalParams); 29 + }, 30 + label(message, ...optionalParams) { 31 + console.log(chalk.blue(message), ...optionalParams); 32 + }, 33 + title(message, ...optionalParams) { 34 + console.log(chalk.blue.bold(message), ...optionalParams); 35 + }, 36 + warn(message, ...optionalParams) { 37 + console.warn(chalk.yellow(message), ...optionalParams); 38 + }, 39 + text: console.log, 40 + };
+15 -3
cli/src/index.ts
··· 1 - import chalk from "chalk"; 2 1 import { Command } from "commander"; 2 + import { cssDocsCommand } from "./commands/css-docs.js"; 3 3 import { docsCommand } from "./commands/docs.js"; 4 4 import { statsCommand } from "./commands/stats.js"; 5 + import { echo } from "./console/echo.js"; 5 6 6 7 const program = new Command(); 7 8 ··· 11 12 try { 12 13 await docsCommand(); 13 14 } catch (error) { 14 - console.error(chalk.red("Error generating docs:"), error); 15 + echo.err("Error generating docs:", error); 15 16 process.exit(1); 16 17 } 17 18 }); ··· 23 24 try { 24 25 await statsCommand(options.full); 25 26 } catch (error) { 26 - console.error(chalk.red("Error generating stats:"), error); 27 + echo.err("Error generating stats:", error); 27 28 process.exit(1); 28 29 } 29 30 }); 31 + 32 + program.command("css-docs").description("Generate CSS documentation from base.css comments and variables").action( 33 + async () => { 34 + try { 35 + await cssDocsCommand(); 36 + } catch (error) { 37 + echo.err("Error generating CSS docs:", error); 38 + process.exit(1); 39 + } 40 + }, 41 + ); 30 42 31 43 program.parse();
+338
docs/css/semantics.md
··· 1 + # Volt CSS Semantics 2 + 3 + Auto-generated documentation from base.css 4 + 5 + ## CSS Custom Properties 6 + 7 + All design tokens defined in the stylesheet. 8 + 9 + ### Typography 10 + 11 + - `--font-size-base`: `18px` 12 + - `--font-size-sm`: `0.889rem` 13 + - `--font-size-lg`: `1.125rem` 14 + - `--font-size-xl`: `1.266rem` 15 + - `--font-size-2xl`: `1.424rem` 16 + - `--font-size-3xl`: `1.802rem` 17 + - `--font-size-4xl`: `2.027rem` 18 + - `--font-size-5xl`: `2.566rem` 19 + - `--font-sans`: `"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` 20 + - `--font-serif`: `"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif` 21 + - `--font-mono`: `"SF Mono", "Cascadia Code", "Fira Code", "Roboto Mono", Consolas, monospace` 22 + - `--line-height-tight`: `1.25` 23 + - `--line-height-base`: `1.6` 24 + - `--line-height-relaxed`: `1.8` 25 + - `--font-size-base`: `16px` 26 + - `--font-size-base`: `15px` 27 + 28 + ### Spacing 29 + 30 + - `--space-xs`: `0.25rem` 31 + - `--space-sm`: `0.5rem` 32 + - `--space-md`: `1rem` 33 + - `--space-lg`: `1.5rem` 34 + - `--space-xl`: `2rem` 35 + - `--space-2xl`: `3rem` 36 + - `--space-3xl`: `4rem` 37 + - `--space-2xl`: `2rem` 38 + - `--space-3xl`: `3rem` 39 + 40 + ### Layout 41 + 42 + - `--content-width`: `70ch` 43 + - `--sidenote-width`: `18rem` 44 + - `--sidenote-gap`: `2rem` 45 + 46 + ### Colors 47 + 48 + - `--color-bg`: `#fefefe` 49 + - `--color-bg-alt`: `#f5f5f5` 50 + - `--color-text`: `#1a1a1a` 51 + - `--color-text-muted`: `#666666` 52 + - `--color-accent`: `#0066cc` 53 + - `--color-accent-hover`: `#0052a3` 54 + - `--color-border`: `#d4d4d4` 55 + - `--color-code-bg`: `#f8f8f8` 56 + - `--color-mark`: `#fff3cd` 57 + - `--color-success`: `#22863a` 58 + - `--color-warning`: `#bf8700` 59 + - `--color-error`: `#cb2431` 60 + - `--color-bg`: `#1a1a1a` 61 + - `--color-bg-alt`: `#2a2a2a` 62 + - `--color-text`: `#e6e6e6` 63 + - `--color-text-muted`: `#a0a0a0` 64 + - `--color-accent`: `#4da6ff` 65 + - `--color-accent-hover`: `#66b3ff` 66 + - `--color-border`: `#404040` 67 + - `--color-code-bg`: `#2a2a2a` 68 + - `--color-mark`: `#4a4a00` 69 + - `--color-success`: `#34d058` 70 + - `--color-warning`: `#ffdf5d` 71 + - `--color-error`: `#f97583` 72 + 73 + ### Effects 74 + 75 + - `--shadow-sm`: `0 1px 2px rgba(0, 0, 0, 0.05)` 76 + - `--shadow-md`: `0 4px 6px rgba(0, 0, 0, 0.07)` 77 + - `--shadow-lg`: `0 10px 15px rgba(0, 0, 0, 0.1)` 78 + - `--radius-sm`: `3px` 79 + - `--radius-md`: `6px` 80 + - `--radius-lg`: `8px` 81 + - `--transition-fast`: `150ms ease-in-out` 82 + - `--transition-base`: `250ms ease-in-out` 83 + - `--shadow-sm`: `0 1px 2px rgba(0, 0, 0, 0.3)` 84 + - `--shadow-md`: `0 4px 6px rgba(0, 0, 0, 0.4)` 85 + - `--shadow-lg`: `0 10px 15px rgba(0, 0, 0, 0.5)` 86 + 87 + ## Element Coverage 88 + 89 + HTML elements with defined styling in the stylesheet. 90 + 91 + **Coverage**: 58/60 elements 92 + 93 + ### Styled Elements 94 + 95 + **Document Structure**: html, body 96 + **Typography**: h1, h2, h3, h4, h5, h6, p, a, em, strong, mark, small, sub, sup, hr 97 + **Lists**: ul, ol, li, dl, dt, dd 98 + **Semantic**: blockquote, cite, article, section, aside, header, footer, nav, details, summary 99 + **Forms**: form, fieldset, legend, label, input, select, textarea, button 100 + **Tables**: table, thead, th, td 101 + **Media**: img, figure, figcaption, video, audio, canvas, svg, iframe 102 + **Code**: code, pre, kbd, samp, var 103 + 104 + ### Unstyled Elements 105 + 106 + tbody, tr 107 + 108 + ## Documentation Comments 109 + 110 + Inline documentation extracted from CSS comments. 111 + 112 + ### `:root` 113 + 114 + Root-level CSS variables define the design system. Light theme is default, dark theme overrides via media query. 115 + 116 + ### `@media (prefers-color-scheme: dark)` 117 + 118 + Dark Theme Overrides Automatically applied when user prefers dark color scheme 119 + 120 + ### `*, *::before, *::after` 121 + 122 + Modern CSS reset with sensible defaults 123 + 124 + ### `html` 125 + 126 + Document root configuration Sets base font size for rem calculations 127 + 128 + ### `body` 129 + 130 + Body element - Primary container Sets default typography and colors for the entire document 131 + 132 + ### `h1, h2, h3, h4, h5, h6` 133 + 134 + Headings hierarchy Uses modular scale for harmonious sizing Tighter line-height for larger text improves readability 135 + 136 + ### `h1` 137 + 138 + Individual heading sizes h1-h3 use slightly larger weights for emphasis 139 + 140 + ### `p` 141 + 142 + Paragraph spacing Generous spacing between paragraphs aids scanning 143 + 144 + ### `h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p` 145 + 146 + First paragraph after headings - No top margin Common convention in academic typography 147 + 148 + ### `a` 149 + 150 + Links - Accessible and distinctive Uses accent color with underline for clarity 151 + 152 + ### `em` 153 + 154 + Emphasis and strong elements 155 + 156 + ### `mark` 157 + 158 + Marked/highlighted text 159 + 160 + ### `sub, sup` 161 + 162 + Subscript and superscript Prevents them from affecting line height 163 + 164 + ### `small` 165 + 166 + Small text Also used for Tufte-style sidenotes (see sidenotes section) 167 + 168 + ### `ul, ol` 169 + 170 + List spacing and indentation Nested lists inherit proper spacing 171 + 172 + ### `li` 173 + 174 + List items 175 + 176 + ### `li > ul, li > ol` 177 + 178 + Nested lists - Reduced spacing 179 + 180 + ### `dl` 181 + 182 + Description lists - For key-value pairs 183 + 184 + ### `p:has(small)` 185 + 186 + Parent paragraph must be positioned for absolute children 187 + 188 + ### `p small` 189 + 190 + Pull small elements into the right margin Creates classic Tufte-style sidenote layout 191 + 192 + ### `@media (max-width: 767px)` 193 + 194 + Mobile sidenotes - Inline with subtle styling 195 + 196 + ### `blockquote` 197 + 198 + Blockquote styling Left border for visual distinction, italic for emphasis 199 + 200 + ### `cite` 201 + 202 + Citation element 203 + 204 + ### `code` 205 + 206 + Inline code Monospace font with subtle background for distinction 207 + 208 + ### `kbd` 209 + 210 + Keyboard input Styled like keys on a keyboard 211 + 212 + ### `samp` 213 + 214 + Sample output 215 + 216 + ### `var` 217 + 218 + Variable 219 + 220 + ### `pre` 221 + 222 + Preformatted code blocks Horizontal scrolling for overflow, no word wrap 223 + 224 + ### `hr` 225 + 226 + Section dividers Centered decorative element with breathing room 227 + 228 + ### `table` 229 + 230 + Table container for horizontal scrolling on small screens 231 + 232 + ### `thead` 233 + 234 + Table header styling Bold text with bottom border for separation 235 + 236 + ### `td` 237 + 238 + Table cells 239 + 240 + ### `tbody tr:nth-child(even)` 241 + 242 + Zebra striping for easier row scanning 243 + 244 + ### `tbody tr:hover` 245 + 246 + Hover state for interactive tables 247 + 248 + ### `form` 249 + 250 + Form container spacing 251 + 252 + ### `fieldset` 253 + 254 + Fieldset grouping 255 + 256 + ### `label` 257 + 258 + Labels Block display for better touch targets 259 + 260 + ### `textarea` 261 + 262 + Textarea specific 263 + 264 + ### `input[type="checkbox"],` 265 + 266 + Checkboxes and radio buttons 267 + 268 + ### `input[type="file"]` 269 + 270 + File input 271 + 272 + ### `input[type="range"]` 273 + 274 + Range input 275 + 276 + ### `progress, meter` 277 + 278 + Progress and meter 279 + 280 + ### `input[type="reset"]` 281 + 282 + Reset button - Subdued styling 283 + 284 + ### `img` 285 + 286 + Images Responsive by default, maintains aspect ratio 287 + 288 + ### `figure` 289 + 290 + Figures with captions Common in academic and technical writing 291 + 292 + ### `video, audio` 293 + 294 + Video and audio Responsive and accessible 295 + 296 + ### `canvas, svg` 297 + 298 + Canvas and SVG 299 + 300 + ### `iframe` 301 + 302 + iframe - Responsive wrapper 303 + 304 + ### `article, section` 305 + 306 + Article and Section Spacing between major content blocks 307 + 308 + ### `aside` 309 + 310 + Aside Complementary content, styled distinctly 311 + 312 + ### `header` 313 + 314 + Header and Footer 315 + 316 + ### `nav` 317 + 318 + Nav Navigation menus 319 + 320 + ### `details` 321 + 322 + Details and Summary Disclosure widget for expandable content 323 + 324 + ### `.sr-only` 325 + 326 + Screen reader only Hides content visually but keeps it accessible to assistive technology 327 + 328 + ### `@media print` 329 + 330 + Print-specific optimizations 331 + 332 + ### `@media (max-width: 768px)` 333 + 334 + Tablet and below - Reduce spacing 335 + 336 + ### `@media (max-width: 480px)` 337 + 338 + Mobile - Further reduced spacing and sizing
+329
docs/css/volt-css.md
··· 1 + # Volt CSS 2 + 3 + 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. 4 + 5 + ## Philosophy 6 + 7 + 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. 8 + 9 + ### Core Principles 10 + 11 + - **Classless**: Style semantic HTML elements directly. 12 + - Optimized line lengths, modular type scale, and generous whitespace optimized for reading 13 + - Automatic light and dark modes via `prefers-color-scheme` to respect user preferences with carefully calibrated color palettes for both modes. 14 + - **Accessibility**: WCAG AA contrast ratios, keyboard navigation support, and semantic HTML patterns 15 + - Mobile-first (ish) design that adapts gracefully from phones to wide desktop monitors without compromising readability. 16 + 17 + ## Inspiration 18 + 19 + Volt CSS synthesizes ideas from several excellent classless CSS frameworks: 20 + 21 + - **magick.css**: Tufte-style [sidenotes](#tufte-style-sidenotes), playful personality, well-commented code 22 + - **LaTeX.css**: Academic typography, wide margins, optimized for technical content 23 + - **Sakura**: Minimal duotone color palettes, rapid prototyping 24 + - **Matcha**: Semantic hierarchy, CSS custom properties architecture 25 + - **MVP.css**: Sensible defaults, zero configuration needed 26 + 27 + ## Features 28 + 29 + ### Complete Element Coverage 30 + 31 + All semantic HTML5 elements are styled out of the box: 32 + 33 + - Typography: headings, paragraphs, links, lists (ordered, unordered, description) 34 + - Content: blockquotes, code blocks, tables, figures with captions 35 + - Forms: inputs, textareas, selects, buttons, checkboxes, radio buttons, file uploads 36 + - Media: images, video, audio, iframes 37 + - Semantic: article, section, aside, header, footer, nav, details/summary 38 + 39 + ### Tufte-Style Sidenotes 40 + 41 + Inspired by Edward Tufte's beautiful book design, margin notes can be added using simple `<small>` elements within paragraphs. 42 + 43 + **Desktop**: Notes appear in the right margin 44 + **Mobile**: Notes appear inline with subtle styling 45 + 46 + ### Example 47 + 48 + ```html 49 + <p> 50 + The framework handles reactivity through signals. 51 + <small> 52 + Signals are similar to reactive primitives in Solid.js and Vue 3's 53 + ref() system, but with a simpler API surface. 54 + </small> 55 + This approach keeps the mental model straightforward. 56 + </p> 57 + ``` 58 + 59 + ### Modular Type Scale 60 + 61 + Font sizes use a 1.25 ratio (major third) for "harmonious" hierarchy: 62 + 63 + - Base: `18px` (`1rem`) 64 + - Scale: `0.889rem`, `1.125rem`, `1.266rem`, `1.424rem`, `1.802rem`, `2.027rem`, `2.566rem` 65 + - Headings use larger sizes from the scale, body text uses base and smaller sizes 66 + 67 + ### Optimized Reading Width 68 + 69 + Main content is constrained to approximately 70 characters per line, around the optimal range for comfortable reading. 70 + Sidenotes extend into the right margin when space allows. 71 + 72 + ### Dark Mode Support 73 + 74 + The stylesheet automatically switches to dark mode when the user's system preference is set to dark: 75 + 76 + ```css 77 + @media (prefers-color-scheme: dark) { 78 + /* Dark theme colors applied automatically */ 79 + } 80 + ``` 81 + 82 + Both themes use carefully selected colors with proper contrast ratios for accessibility. 83 + 84 + ## Usage 85 + 86 + ### Basic Setup 87 + 88 + Include the stylesheet in your HTML `<head>`: 89 + 90 + ```html 91 + <!DOCTYPE html> 92 + <html lang="en"> 93 + <head> 94 + <meta charset="UTF-8"> 95 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 96 + <link rel="stylesheet" href="/src/styles/base.css"> 97 + <title>My Document</title> 98 + </head> 99 + <body> 100 + <!-- Your markup --> 101 + </body> 102 + </html> 103 + ``` 104 + 105 + ### Example Document Structure 106 + 107 + ```html 108 + <body> 109 + <header> 110 + <h1>Document Title</h1> 111 + <p>Subtitle or introduction</p> 112 + </header> 113 + 114 + <article> 115 + <h2>Section Heading</h2> 116 + <p> 117 + Your content flows naturally. 118 + <small>Add sidenotes for additional context.</small> 119 + The stylesheet handles all the styling. 120 + </p> 121 + 122 + <blockquote> 123 + <p>Quotes are styled with subtle backgrounds and borders.</p> 124 + <cite>Author Name</cite> 125 + </blockquote> 126 + 127 + <pre><code>// Code blocks use monospace fonts 128 + const example = "syntax highlighting not included";</code></pre> 129 + 130 + <table> 131 + <thead> 132 + <tr> 133 + <th>Feature</th> 134 + <th>Status</th> 135 + </tr> 136 + </thead> 137 + <tbody> 138 + <tr> 139 + <td>Tables</td> 140 + <td>Styled with zebra striping</td> 141 + </tr> 142 + </tbody> 143 + </table> 144 + </article> 145 + 146 + <footer> 147 + <p>Footer content, copyright, etc.</p> 148 + </footer> 149 + </body> 150 + ``` 151 + 152 + ### Forms 153 + 154 + Forms get styling with focus states, required field indicators, and proper spacing: 155 + 156 + ```html 157 + <form> 158 + <fieldset> 159 + <legend>User Information</legend> 160 + 161 + <label for="name">Name</label> 162 + <input type="text" id="name" name="name" required> 163 + 164 + <label for="email">Email</label> 165 + <input type="email" id="email" name="email" required> 166 + 167 + <label for="message">Message</label> 168 + <textarea id="message" name="message"></textarea> 169 + 170 + <button type="submit">Send Message</button> 171 + <input type="reset" value="Clear Form"> 172 + </fieldset> 173 + </form> 174 + ``` 175 + 176 + ## Customization 177 + 178 + ### CSS Custom Properties 179 + 180 + All design tokens are defined as CSS custom properties (CSS variables) in the `:root` selector. Override them to customize the appearance: 181 + 182 + ```css 183 + :root { 184 + /* Change the accent color */ 185 + --color-accent: #d63384; 186 + --color-accent-hover: #b02a6b; 187 + 188 + /* Adjust spacing */ 189 + --space-md: 1.25rem; 190 + 191 + /* Change fonts */ 192 + --font-sans: "Your Font", system-ui, sans-serif; 193 + 194 + /* Modify content width */ 195 + --content-width: 60ch; 196 + } 197 + ``` 198 + 199 + ### Properties 200 + 201 + **Typography**: 202 + 203 + - `--font-sans`, `--font-serif`, `--font-mono`: Font families 204 + - `--font-size-*`: Size scale from sm to 5xl 205 + - `--line-height-tight`, `--line-height-base`, `--line-height-relaxed` 206 + 207 + **Colors**: 208 + 209 + - `--color-bg`: Background color 210 + - `--color-bg-alt`: Alternate background (code blocks, table stripes) 211 + - `--color-text`: Primary text color 212 + - `--color-text-muted`: Secondary text color 213 + - `--color-accent`: Accent color for links, buttons 214 + - `--color-accent-hover`: Hover state for accent color 215 + - `--color-border`: Border color 216 + - `--color-code-bg`: Code block background 217 + - `--color-mark`: Highlighted text background 218 + - `--color-success`, `--color-warning`, `--color-error`: Semantic colors 219 + 220 + **Spacing**: 221 + 222 + - `--space-xs` through `--space-3xl`: Spacing scale 223 + 224 + **Layout**: 225 + 226 + - `--content-width`: Maximum width for readable content 227 + - `--sidenote-width`: Width of margin notes 228 + - `--sidenote-gap`: Space between content and sidenotes 229 + 230 + **Effects**: 231 + 232 + - `--shadow-sm`, `--shadow-md`, `--shadow-lg`: Box shadows 233 + - `--radius-sm`, `--radius-md`, `--radius-lg`: Border radius 234 + - `--transition-fast`, `--transition-base`: Transition durations 235 + 236 + ### Dark Mode Customization 237 + 238 + Override dark mode colors specifically: 239 + 240 + ```css 241 + @media (prefers-color-scheme: dark) { 242 + :root { 243 + --color-accent: #f0f; 244 + --color-bg: #000; 245 + } 246 + } 247 + ``` 248 + 249 + ### Scoped Customization 250 + 251 + Apply custom styling to specific sections without affecting the whole document: 252 + 253 + ```html 254 + <style> 255 + .special-section { 256 + --color-accent: #e74c3c; 257 + --font-sans: "Georgia", serif; 258 + } 259 + </style> 260 + 261 + <div class="special-section"> 262 + <h2>This section uses different colors and fonts</h2> 263 + <p>All nested elements inherit the custom properties.</p> 264 + </div> 265 + ``` 266 + 267 + ## Browser Support 268 + 269 + Volt CSS uses modern CSS features and targets evergreen browsers: 270 + 271 + - Chrome/Edge 90+ 272 + - Firefox 88+ 273 + - Safari 14+ 274 + 275 + Specifically relies on: 276 + 277 + - CSS custom properties (CSS variables) 278 + - CSS Grid and Flexbox 279 + - `:has()` selector (for sidenote positioning) 280 + - `prefers-color-scheme` media query 281 + 282 + For older browsers, content remains readable but may lack advanced layout features like margin sidenotes. 283 + 284 + ## Accessibility 285 + 286 + - All color combinations meet WCAG AA standards (4.5:1 for normal text, 3:1 for large text) 287 + - Clear, visible focus states for keyboard navigation 288 + - Encourages proper heading hierarchy, landmark regions, and form labels 289 + - Works on all devices and respects user font size preferences 290 + - No animations that could trigger vestibular disorders 291 + 292 + ## Size & Performance 293 + 294 + The complete stylesheet is approximately 15KB uncompressed, 3-4KB when gzipped 295 + 296 + For maximum performance: 297 + 298 + 1. Serve with proper compression (gzip or brotli) 299 + 2. Set appropriate cache headers 300 + 3. Consider inlining in `<style>` tags for above-the-fold content on single-page sites 301 + 302 + ## Design Decisions 303 + 304 + ### Sans-Serif 305 + 306 + While serif fonts are traditional for long-form reading, modern screens render sans-serif fonts with excellent clarity. The system font stack ensures fast loading and familiar reading experience while maintaining personality through spacing, hierarchy, and layout. 307 + 308 + ### `<small>` Sidenotes? 309 + 310 + The `<small>` element semantically represents side comments and fine print, making it a natural choice for sidenotes. This approach requires no custom attributes or classes, keeping markup clean and portable. 311 + 312 + ### 70 Characters Line Length 313 + 314 + Research shows optimal reading comprehension occurs with 45-75 characters per line. 70 characters balances readability with efficient use of screen space. 315 + 316 + ### Automatic Dark Mode 317 + 318 + Respecting user preferences improves accessibility and reduces eye strain. Automatic theme switching via `prefers-color-scheme` requires zero configuration while providing the best experience for each user. 319 + 320 + ## License 321 + 322 + Part of the Volt.js project. MIT licensed. 323 + 324 + ## Further Reading 325 + 326 + - [Tufte CSS](https://edwardtufte.github.io/tufte-css/) 327 + - [Practical Typography by Matthew Butterick](https://practicaltypography.com/) 328 + - [The Elements of Typographic Style Applied to the Web](http://webtypography.net/) 329 + - [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/WAI/WCAG21/quickref/)
+39 -63
index.html
··· 5 5 <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 7 <title>Volt.js Demo</title> 8 + <link rel="stylesheet" href="/src/styles/base.css" /> 8 9 <style> 9 - body { 10 - font-family: system-ui, -apple-system, sans-serif; 11 - max-width: 800px; 12 - margin: 2rem auto; 13 - padding: 0 1rem; 14 - line-height: 1.6; 15 - } 16 - .card { 17 - border: 1px solid #ddd; 18 - border-radius: 8px; 19 - padding: 1.5rem; 20 - margin: 1rem 0; 21 - } 22 - button { 23 - background: #646cff; 24 - color: white; 25 - border: none; 26 - padding: 0.6rem 1.2rem; 27 - border-radius: 4px; 28 - cursor: pointer; 29 - margin: 0.5rem 0.5rem 0.5rem 0; 30 - font-size: 1rem; 31 - } 32 - button:hover { 33 - background: #535bf2; 34 - } 10 + /* Demo-specific utility classes */ 35 11 .active { 36 - color: #646cff; 12 + color: var(--color-accent); 37 13 font-weight: bold; 38 14 } 39 15 .highlight { 40 - background: #fff3cd; 16 + background: var(--color-mark); 41 17 padding: 0.25rem 0.5rem; 42 - border-radius: 4px; 18 + border-radius: var(--radius-sm); 43 19 } 44 20 </style> 45 21 </head> 46 22 <body> 47 23 <div id="app"> 48 - <h1 data-x-text="message">Loading...</h1> 24 + <header> 25 + <h1 data-x-text="message">Loading...</h1> 26 + <p>A reactive framework demo powered by Volt.js</p> 27 + </header> 49 28 50 - <div class="card"> 51 - <h2>Event Bindings & Computed Values</h2> 52 - <p> 53 - Count: <strong data-x-text="count">0</strong><br /> 54 - Doubled: <strong data-x-text="doubled">0</strong> 55 - </p> 56 - <button data-x-on-click="increment">Increment</button> 57 - <button data-x-on-click="decrement">Decrement</button> 58 - <button data-x-on-click="reset">Reset</button> 59 - <button data-x-on-click="updateMessage">Update Message</button> 60 - </div> 29 + <article> 30 + <section> 31 + <h2>Event Bindings & Computed Values</h2> 32 + <p> 33 + Count: <strong data-x-text="count">0</strong><br /> 34 + Doubled: <strong data-x-text="doubled">0</strong> 35 + </p> 36 + <button data-x-on-click="increment">Increment</button> 37 + <button data-x-on-click="decrement">Decrement</button> 38 + <button data-x-on-click="reset">Reset</button> 39 + <button data-x-on-click="updateMessage">Update Message</button> 40 + </section> 61 41 62 - <div class="card"> 63 - <h2>Form Input</h2> 64 - <input 65 - type="text" 66 - data-x-on-input="handleInput" 67 - placeholder="Type something..." 68 - style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; width: 100%" 69 - /> 70 - <p>You typed: <strong data-x-text="inputValue">nothing yet</strong></p> 71 - </div> 42 + <section> 43 + <h2>Form Input</h2> 44 + <input type="text" data-x-on-input="handleInput" placeholder="Type something..." /> 45 + <p>You typed: <strong data-x-text="inputValue">nothing yet</strong></p> 46 + </section> 72 47 73 - <div class="card"> 74 - <h2>Class Bindings</h2> 75 - <p data-x-class="classes">This text has dynamic classes applied.</p> 76 - <p> 77 - Active: <span data-x-text="isActive">false</span> 78 - </p> 79 - <button data-x-on-click="toggleActive">Toggle Active</button> 80 - </div> 48 + <section> 49 + <h2>Class Bindings</h2> 50 + <p data-x-class="classes">This text has dynamic classes applied.</p> 51 + <p> 52 + Active: <span data-x-text="isActive">false</span> 53 + </p> 54 + <button data-x-on-click="toggleActive">Toggle Active</button> 55 + </section> 81 56 82 - <div class="card"> 83 - <h2>HTML Binding</h2> 84 - <div data-x-html="'<em>This is rendered as HTML</em>'">Fallback content</div> 85 - </div> 57 + <section> 58 + <h2>HTML Binding</h2> 59 + <div data-x-html="'<em>This is rendered as HTML</em>'">Fallback content</div> 60 + </section> 61 + </article> 86 62 </div> 87 63 <script type="module" src="/src/main.ts"></script> 88 64 </body>
+1070
src/styles/base.css
··· 1 + /** 2 + * Volt CSS - Classless stylesheet for elegant, readable web documents 3 + * 4 + * Design Philosophy: 5 + * - Classless: Style semantic HTML5 elements directly 6 + * - Dual theme: Automatic light/dark mode via prefers-color-scheme 7 + * - Typography-first: Optimized for reading and information density 8 + * - Accessible: WCAG AA contrast ratios, keyboard navigation support 9 + * - Responsive: Mobile-first, adapts gracefully to all screen sizes 10 + * 11 + * Inspired by: magick.css, latex-css, sakura, matcha, mvp.css 12 + */ 13 + 14 + /* ========================================================================== 15 + CSS Custom Properties - Design Tokens 16 + ========================================================================== */ 17 + 18 + /** 19 + * Root-level CSS variables define the design system. 20 + * Light theme is default, dark theme overrides via media query. 21 + */ 22 + :root { 23 + /* Typography Scale - Modular scale based on 1.25 ratio */ 24 + --font-size-base: 18px; 25 + --font-size-sm: 0.889rem; /* 16px */ 26 + --font-size-lg: 1.125rem; /* 20.25px */ 27 + --font-size-xl: 1.266rem; /* 22.8px */ 28 + --font-size-2xl: 1.424rem; /* 25.6px */ 29 + --font-size-3xl: 1.802rem; /* 32.4px */ 30 + --font-size-4xl: 2.027rem; /* 36.5px */ 31 + --font-size-5xl: 2.566rem; /* 46.2px */ 32 + 33 + /* Font Families - Sans-serif with personality */ 34 + /* System fonts for performance, fallback to serif for character */ 35 + --font-sans: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 36 + --font-serif: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif; 37 + --font-mono: "SF Mono", "Cascadia Code", "Fira Code", "Roboto Mono", Consolas, monospace; 38 + 39 + /* Spacing Scale - Based on 0.5rem increments */ 40 + --space-xs: 0.25rem; /* 4px */ 41 + --space-sm: 0.5rem; /* 8px */ 42 + --space-md: 1rem; /* 16px */ 43 + --space-lg: 1.5rem; /* 24px */ 44 + --space-xl: 2rem; /* 32px */ 45 + --space-2xl: 3rem; /* 48px */ 46 + --space-3xl: 4rem; /* 64px */ 47 + 48 + /* Line Heights - Optimized for readability */ 49 + --line-height-tight: 1.25; 50 + --line-height-base: 1.6; 51 + --line-height-relaxed: 1.8; 52 + 53 + /* Layout Dimensions */ 54 + --content-width: 70ch; /* Optimal line length for reading */ 55 + --sidenote-width: 18rem; /* Width of margin notes */ 56 + --sidenote-gap: 2rem; /* Space between content and sidenotes */ 57 + 58 + /* Light Theme Colors - Duotone palette for clarity */ 59 + --color-bg: #fefefe; 60 + --color-bg-alt: #f5f5f5; /* For code blocks, tables */ 61 + --color-text: #1a1a1a; 62 + --color-text-muted: #666666; 63 + --color-accent: #0066cc; /* Links, primary actions */ 64 + --color-accent-hover: #0052a3; 65 + --color-border: #d4d4d4; 66 + --color-code-bg: #f8f8f8; 67 + --color-mark: #fff3cd; /* Highlighted text */ 68 + --color-success: #22863a; 69 + --color-warning: #bf8700; 70 + --color-error: #cb2431; 71 + 72 + /* Shadows - Subtle depth */ 73 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); 74 + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); 75 + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); 76 + 77 + /* Border Radius */ 78 + --radius-sm: 3px; 79 + --radius-md: 6px; 80 + --radius-lg: 8px; 81 + 82 + /* Transitions */ 83 + --transition-fast: 150ms ease-in-out; 84 + --transition-base: 250ms ease-in-out; 85 + } 86 + 87 + /** 88 + * Dark Theme Overrides 89 + * Automatically applied when user prefers dark color scheme 90 + */ 91 + @media (prefers-color-scheme: dark) { 92 + :root { 93 + --color-bg: #1a1a1a; 94 + --color-bg-alt: #2a2a2a; 95 + --color-text: #e6e6e6; 96 + --color-text-muted: #a0a0a0; 97 + --color-accent: #4da6ff; 98 + --color-accent-hover: #66b3ff; 99 + --color-border: #404040; 100 + --color-code-bg: #2a2a2a; 101 + --color-mark: #4a4a00; 102 + --color-success: #34d058; 103 + --color-warning: #ffdf5d; 104 + --color-error: #f97583; 105 + 106 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); 107 + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); 108 + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5); 109 + } 110 + } 111 + 112 + /* ========================================================================== 113 + CSS Reset & Base Styles 114 + ========================================================================== */ 115 + 116 + /** 117 + * Modern CSS reset with sensible defaults 118 + */ 119 + *, *::before, *::after { 120 + box-sizing: border-box; 121 + } 122 + 123 + * { 124 + margin: 0; 125 + padding: 0; 126 + } 127 + 128 + /** 129 + * Document root configuration 130 + * Sets base font size for rem calculations 131 + */ 132 + html { 133 + font-size: var(--font-size-base); 134 + -webkit-text-size-adjust: 100%; 135 + -webkit-font-smoothing: antialiased; 136 + -moz-osx-font-smoothing: grayscale; 137 + text-rendering: optimizeLegibility; 138 + } 139 + 140 + /** 141 + * Body element - Primary container 142 + * Sets default typography and colors for the entire document 143 + */ 144 + body { 145 + font-family: var(--font-sans); 146 + font-size: 1rem; 147 + line-height: var(--line-height-base); 148 + color: var(--color-text); 149 + background-color: var(--color-bg); 150 + 151 + /* Center content with optimal reading width */ 152 + max-width: calc(var(--content-width) + var(--sidenote-width) + var(--sidenote-gap) * 2); 153 + margin: 0 auto; 154 + padding: var(--space-2xl) var(--space-lg); 155 + } 156 + 157 + /* ========================================================================== 158 + Typography - Hierarchy & Rhythm 159 + ========================================================================== */ 160 + 161 + /** 162 + * Headings hierarchy 163 + * Uses modular scale for harmonious sizing 164 + * Tighter line-height for larger text improves readability 165 + */ 166 + h1, h2, h3, h4, h5, h6 { 167 + font-weight: 700; 168 + line-height: var(--line-height-tight); 169 + color: var(--color-text); 170 + margin-top: var(--space-2xl); 171 + margin-bottom: var(--space-md); 172 + letter-spacing: -0.02em; /* Slight negative tracking for display text */ 173 + } 174 + 175 + /** 176 + * Individual heading sizes 177 + * h1-h3 use slightly larger weights for emphasis 178 + */ 179 + h1 { 180 + font-size: var(--font-size-5xl); 181 + margin-top: 0; /* No top margin on first heading */ 182 + } 183 + 184 + h2 { 185 + font-size: var(--font-size-4xl); 186 + } 187 + 188 + h3 { 189 + font-size: var(--font-size-3xl); 190 + } 191 + 192 + h4 { 193 + font-size: var(--font-size-2xl); 194 + } 195 + 196 + h5 { 197 + font-size: var(--font-size-xl); 198 + } 199 + 200 + h6 { 201 + font-size: var(--font-size-lg); 202 + color: var(--color-text-muted); 203 + text-transform: uppercase; 204 + letter-spacing: 0.05em; 205 + } 206 + 207 + /** 208 + * Paragraph spacing 209 + * Generous spacing between paragraphs aids scanning 210 + */ 211 + p { 212 + margin-bottom: var(--space-lg); 213 + max-width: var(--content-width); 214 + } 215 + 216 + /** 217 + * First paragraph after headings - No top margin 218 + * Common convention in academic typography 219 + */ 220 + h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p { 221 + margin-top: 0; 222 + } 223 + 224 + /** 225 + * Links - Accessible and distinctive 226 + * Uses accent color with underline for clarity 227 + */ 228 + a { 229 + color: var(--color-accent); 230 + text-decoration: underline; 231 + text-decoration-thickness: 1px; 232 + text-underline-offset: 2px; 233 + transition: color var(--transition-fast); 234 + } 235 + 236 + a:hover { 237 + color: var(--color-accent-hover); 238 + } 239 + 240 + a:focus-visible { 241 + outline: 2px solid var(--color-accent); 242 + outline-offset: 2px; 243 + border-radius: var(--radius-sm); 244 + } 245 + 246 + /** 247 + * Emphasis and strong elements 248 + */ 249 + em { 250 + font-style: italic; 251 + } 252 + 253 + strong { 254 + font-weight: 700; 255 + } 256 + 257 + /** 258 + * Marked/highlighted text 259 + */ 260 + mark { 261 + background-color: var(--color-mark); 262 + padding: 0.1em 0.2em; 263 + border-radius: var(--radius-sm); 264 + } 265 + 266 + /** 267 + * Subscript and superscript 268 + * Prevents them from affecting line height 269 + */ 270 + sub, sup { 271 + font-size: 0.75em; 272 + line-height: 0; 273 + position: relative; 274 + vertical-align: baseline; 275 + } 276 + 277 + sup { 278 + top: -0.5em; 279 + } 280 + 281 + sub { 282 + bottom: -0.25em; 283 + } 284 + 285 + /** 286 + * Small text 287 + * Also used for Tufte-style sidenotes (see sidenotes section) 288 + */ 289 + small { 290 + font-size: var(--font-size-sm); 291 + color: var(--color-text-muted); 292 + } 293 + 294 + /* ========================================================================== 295 + Lists - Ordered & Unordered 296 + ========================================================================== */ 297 + 298 + /** 299 + * List spacing and indentation 300 + * Nested lists inherit proper spacing 301 + */ 302 + ul, ol { 303 + margin-bottom: var(--space-lg); 304 + padding-left: var(--space-xl); 305 + max-width: var(--content-width); 306 + } 307 + 308 + /** 309 + * List items 310 + */ 311 + li { 312 + margin-bottom: var(--space-sm); 313 + } 314 + 315 + li::marker { 316 + color: var(--color-accent); 317 + } 318 + 319 + /** 320 + * Nested lists - Reduced spacing 321 + */ 322 + li > ul, li > ol { 323 + margin-top: var(--space-sm); 324 + margin-bottom: var(--space-sm); 325 + } 326 + 327 + /** 328 + * Description lists - For key-value pairs 329 + */ 330 + dl { 331 + margin-bottom: var(--space-lg); 332 + max-width: var(--content-width); 333 + } 334 + 335 + dt { 336 + font-weight: 700; 337 + margin-top: var(--space-md); 338 + margin-bottom: var(--space-xs); 339 + } 340 + 341 + dd { 342 + margin-left: var(--space-xl); 343 + margin-bottom: var(--space-sm); 344 + color: var(--color-text-muted); 345 + } 346 + 347 + /* ========================================================================== 348 + Tufte-Style Sidenotes 349 + ========================================================================== */ 350 + 351 + /** 352 + * Sidenotes using <small> elements 353 + * On desktop: positioned in right margin 354 + * On mobile: inline with reduced emphasis 355 + * 356 + * Usage: Place <small> inside <p> where you want the note to appear 357 + * Example: <p>Main text here <small>This appears in margin</small> more text.</p> 358 + */ 359 + @media (min-width: 768px) { 360 + /** 361 + * Parent paragraph must be positioned for absolute children 362 + */ 363 + p:has(small) { 364 + position: relative; 365 + } 366 + 367 + /** 368 + * Pull small elements into the right margin 369 + * Creates classic Tufte-style sidenote layout 370 + */ 371 + p small { 372 + position: absolute; 373 + left: calc(100% + var(--sidenote-gap)); 374 + width: var(--sidenote-width); 375 + font-size: 0.85rem; 376 + line-height: var(--line-height-base); 377 + margin-top: 0; 378 + padding: var(--space-sm); 379 + background-color: var(--color-bg-alt); 380 + border-left: 2px solid var(--color-accent); 381 + border-radius: var(--radius-sm); 382 + } 383 + } 384 + 385 + /** 386 + * Mobile sidenotes - Inline with subtle styling 387 + */ 388 + @media (max-width: 767px) { 389 + p small { 390 + display: block; 391 + margin-top: var(--space-sm); 392 + margin-bottom: var(--space-sm); 393 + padding: var(--space-sm); 394 + background-color: var(--color-bg-alt); 395 + border-left: 2px solid var(--color-accent); 396 + border-radius: var(--radius-sm); 397 + font-size: 0.9rem; 398 + } 399 + } 400 + 401 + /* ========================================================================== 402 + Blockquotes & Citations 403 + ========================================================================== */ 404 + 405 + /** 406 + * Blockquote styling 407 + * Left border for visual distinction, italic for emphasis 408 + */ 409 + blockquote { 410 + margin: var(--space-xl) 0; 411 + padding: var(--space-md) var(--space-lg); 412 + border-left: 4px solid var(--color-accent); 413 + background-color: var(--color-bg-alt); 414 + font-style: italic; 415 + color: var(--color-text-muted); 416 + max-width: var(--content-width); 417 + border-radius: var(--radius-sm); 418 + } 419 + 420 + blockquote p:last-child { 421 + margin-bottom: 0; 422 + } 423 + 424 + /** 425 + * Citation element 426 + */ 427 + cite { 428 + font-style: normal; 429 + font-size: var(--font-size-sm); 430 + color: var(--color-text-muted); 431 + } 432 + 433 + blockquote cite::before { 434 + content: " "; 435 + } 436 + 437 + /* ========================================================================== 438 + Code & Preformatted Text 439 + ========================================================================== */ 440 + 441 + /** 442 + * Inline code 443 + * Monospace font with subtle background for distinction 444 + */ 445 + code { 446 + font-family: var(--font-mono); 447 + font-size: 0.9em; 448 + padding: 0.15em 0.4em; 449 + background-color: var(--color-code-bg); 450 + border: 1px solid var(--color-border); 451 + border-radius: var(--radius-sm); 452 + } 453 + 454 + /** 455 + * Keyboard input 456 + * Styled like keys on a keyboard 457 + */ 458 + kbd { 459 + font-family: var(--font-mono); 460 + font-size: 0.9em; 461 + padding: 0.15em 0.4em; 462 + background-color: var(--color-bg-alt); 463 + border: 1px solid var(--color-border); 464 + border-radius: var(--radius-sm); 465 + box-shadow: 0 1px 0 var(--color-border), 0 0 0 2px var(--color-bg) inset; 466 + } 467 + 468 + /** 469 + * Sample output 470 + */ 471 + samp { 472 + font-family: var(--font-mono); 473 + font-size: 0.9em; 474 + } 475 + 476 + /** 477 + * Variable 478 + */ 479 + var { 480 + font-family: var(--font-mono); 481 + font-style: normal; 482 + font-weight: 600; 483 + } 484 + 485 + /** 486 + * Preformatted code blocks 487 + * Horizontal scrolling for overflow, no word wrap 488 + */ 489 + pre { 490 + margin: var(--space-xl) 0; 491 + padding: var(--space-lg); 492 + background-color: var(--color-code-bg); 493 + border: 1px solid var(--color-border); 494 + border-radius: var(--radius-md); 495 + overflow-x: auto; 496 + max-width: var(--content-width); 497 + line-height: var(--line-height-base); 498 + } 499 + 500 + pre code { 501 + padding: 0; 502 + background: none; 503 + border: none; 504 + font-size: 0.875rem; 505 + } 506 + 507 + /* ========================================================================== 508 + Horizontal Rules 509 + ========================================================================== */ 510 + 511 + /** 512 + * Section dividers 513 + * Centered decorative element with breathing room 514 + */ 515 + hr { 516 + margin: var(--space-3xl) auto; 517 + border: none; 518 + border-top: 1px solid var(--color-border); 519 + max-width: 50%; 520 + } 521 + 522 + /* ========================================================================== 523 + Tables 524 + ========================================================================== */ 525 + 526 + /** 527 + * Table container for horizontal scrolling on small screens 528 + */ 529 + table { 530 + width: 100%; 531 + max-width: var(--content-width); 532 + margin: var(--space-xl) 0; 533 + border-collapse: collapse; 534 + overflow-x: auto; 535 + display: block; 536 + } 537 + 538 + /** 539 + * Table header styling 540 + * Bold text with bottom border for separation 541 + */ 542 + thead { 543 + background-color: var(--color-bg-alt); 544 + border-bottom: 2px solid var(--color-border); 545 + } 546 + 547 + th { 548 + padding: var(--space-sm) var(--space-md); 549 + text-align: left; 550 + font-weight: 700; 551 + color: var(--color-text); 552 + } 553 + 554 + /** 555 + * Table cells 556 + */ 557 + td { 558 + padding: var(--space-sm) var(--space-md); 559 + border-bottom: 1px solid var(--color-border); 560 + } 561 + 562 + /** 563 + * Zebra striping for easier row scanning 564 + */ 565 + tbody tr:nth-child(even) { 566 + background-color: var(--color-bg-alt); 567 + } 568 + 569 + /** 570 + * Hover state for interactive tables 571 + */ 572 + tbody tr:hover { 573 + background-color: var(--color-border); 574 + transition: background-color var(--transition-fast); 575 + } 576 + 577 + /* ========================================================================== 578 + Forms & Input Elements 579 + ========================================================================== */ 580 + 581 + /** 582 + * Form container spacing 583 + */ 584 + form { 585 + margin: var(--space-xl) 0; 586 + max-width: var(--content-width); 587 + } 588 + 589 + /** 590 + * Fieldset grouping 591 + */ 592 + fieldset { 593 + border: 1px solid var(--color-border); 594 + border-radius: var(--radius-md); 595 + padding: var(--space-lg); 596 + margin-bottom: var(--space-lg); 597 + } 598 + 599 + legend { 600 + font-weight: 700; 601 + padding: 0 var(--space-sm); 602 + color: var(--color-text); 603 + } 604 + 605 + /** 606 + * Labels 607 + * Block display for better touch targets 608 + */ 609 + label { 610 + display: block; 611 + margin-bottom: var(--space-xs); 612 + font-weight: 600; 613 + color: var(--color-text); 614 + } 615 + 616 + /** 617 + * Required field indicator 618 + */ 619 + label:has(+ input[required])::after, 620 + label:has(+ textarea[required])::after, 621 + label:has(+ select[required])::after { 622 + content: " *"; 623 + color: var(--color-error); 624 + } 625 + 626 + /** 627 + * Text inputs and textareas 628 + * Consistent sizing and interaction states 629 + */ 630 + input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]), 631 + select, 632 + textarea { 633 + width: 100%; 634 + padding: var(--space-sm) var(--space-md); 635 + font-family: inherit; 636 + font-size: 1rem; 637 + line-height: var(--line-height-base); 638 + color: var(--color-text); 639 + background-color: var(--color-bg); 640 + border: 1px solid var(--color-border); 641 + border-radius: var(--radius-sm); 642 + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); 643 + margin-bottom: var(--space-md); 644 + } 645 + 646 + /** 647 + * Focus states for inputs 648 + * Clear visual feedback for keyboard navigation 649 + */ 650 + input:focus, 651 + select:focus, 652 + textarea:focus { 653 + outline: none; 654 + border-color: var(--color-accent); 655 + box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); 656 + } 657 + 658 + /** 659 + * Disabled state 660 + */ 661 + input:disabled, 662 + select:disabled, 663 + textarea:disabled { 664 + opacity: 0.6; 665 + cursor: not-allowed; 666 + background-color: var(--color-bg-alt); 667 + } 668 + 669 + /** 670 + * Textarea specific 671 + */ 672 + textarea { 673 + resize: vertical; 674 + min-height: 8rem; 675 + } 676 + 677 + /** 678 + * Checkboxes and radio buttons 679 + */ 680 + input[type="checkbox"], 681 + input[type="radio"] { 682 + margin-right: var(--space-xs); 683 + accent-color: var(--color-accent); 684 + } 685 + 686 + /** 687 + * File input 688 + */ 689 + input[type="file"] { 690 + padding: var(--space-sm); 691 + border: 1px dashed var(--color-border); 692 + border-radius: var(--radius-sm); 693 + cursor: pointer; 694 + margin-bottom: var(--space-md); 695 + } 696 + 697 + /** 698 + * Range input 699 + */ 700 + input[type="range"] { 701 + width: 100%; 702 + margin: var(--space-md) 0; 703 + accent-color: var(--color-accent); 704 + } 705 + 706 + /** 707 + * Progress and meter 708 + */ 709 + progress, meter { 710 + width: 100%; 711 + height: 1.5rem; 712 + margin: var(--space-md) 0; 713 + border-radius: var(--radius-sm); 714 + overflow: hidden; 715 + } 716 + 717 + /* ========================================================================== 718 + Buttons 719 + ========================================================================== */ 720 + 721 + /** 722 + * Button styling 723 + * Primary action style with hover and active states 724 + */ 725 + button, 726 + input[type="submit"], 727 + input[type="button"], 728 + input[type="reset"] { 729 + display: inline-block; 730 + padding: var(--space-sm) var(--space-lg); 731 + font-family: inherit; 732 + font-size: 1rem; 733 + font-weight: 600; 734 + line-height: 1; 735 + color: white; 736 + background-color: var(--color-accent); 737 + border: none; 738 + border-radius: var(--radius-md); 739 + cursor: pointer; 740 + text-decoration: none; 741 + transition: background-color var(--transition-fast), transform var(--transition-fast); 742 + margin-right: var(--space-sm); 743 + margin-bottom: var(--space-sm); 744 + } 745 + 746 + button:hover, 747 + input[type="submit"]:hover, 748 + input[type="button"]:hover { 749 + background-color: var(--color-accent-hover); 750 + } 751 + 752 + button:active, 753 + input[type="submit"]:active, 754 + input[type="button"]:active { 755 + transform: translateY(1px); 756 + } 757 + 758 + /** 759 + * Reset button - Subdued styling 760 + */ 761 + input[type="reset"] { 762 + background-color: var(--color-bg-alt); 763 + color: var(--color-text); 764 + border: 1px solid var(--color-border); 765 + } 766 + 767 + input[type="reset"]:hover { 768 + background-color: var(--color-border); 769 + } 770 + 771 + /** 772 + * Disabled buttons 773 + */ 774 + button:disabled, 775 + input[type="submit"]:disabled, 776 + input[type="button"]:disabled { 777 + opacity: 0.6; 778 + cursor: not-allowed; 779 + transform: none; 780 + } 781 + 782 + /** 783 + * Focus state for buttons 784 + */ 785 + button:focus-visible, 786 + input[type="submit"]:focus-visible, 787 + input[type="button"]:focus-visible { 788 + outline: 2px solid var(--color-accent); 789 + outline-offset: 2px; 790 + } 791 + 792 + /* ========================================================================== 793 + Media Elements 794 + ========================================================================== */ 795 + 796 + /** 797 + * Images 798 + * Responsive by default, maintains aspect ratio 799 + */ 800 + img { 801 + max-width: 100%; 802 + height: auto; 803 + display: block; 804 + border-radius: var(--radius-sm); 805 + } 806 + 807 + /** 808 + * Figures with captions 809 + * Common in academic and technical writing 810 + */ 811 + figure { 812 + margin: var(--space-xl) 0; 813 + max-width: var(--content-width); 814 + } 815 + 816 + figcaption { 817 + margin-top: var(--space-sm); 818 + font-size: var(--font-size-sm); 819 + color: var(--color-text-muted); 820 + font-style: italic; 821 + text-align: center; 822 + } 823 + 824 + /** 825 + * Video and audio 826 + * Responsive and accessible 827 + */ 828 + video, audio { 829 + max-width: 100%; 830 + margin: var(--space-xl) 0; 831 + } 832 + 833 + /** 834 + * Canvas and SVG 835 + */ 836 + canvas, svg { 837 + max-width: 100%; 838 + height: auto; 839 + } 840 + 841 + /* ========================================================================== 842 + Embedded Content 843 + ========================================================================== */ 844 + 845 + /** 846 + * iframe - Responsive wrapper 847 + */ 848 + iframe { 849 + max-width: 100%; 850 + border: 1px solid var(--color-border); 851 + border-radius: var(--radius-md); 852 + margin: var(--space-xl) 0; 853 + } 854 + 855 + /* ========================================================================== 856 + Semantic HTML5 Elements 857 + ========================================================================== */ 858 + 859 + /** 860 + * Article and Section 861 + * Spacing between major content blocks 862 + */ 863 + article, section { 864 + margin-bottom: var(--space-3xl); 865 + } 866 + 867 + /** 868 + * Aside 869 + * Complementary content, styled distinctly 870 + */ 871 + aside { 872 + padding: var(--space-lg); 873 + margin: var(--space-xl) 0; 874 + background-color: var(--color-bg-alt); 875 + border-left: 4px solid var(--color-accent); 876 + border-radius: var(--radius-sm); 877 + max-width: var(--content-width); 878 + } 879 + 880 + /** 881 + * Header and Footer 882 + */ 883 + header { 884 + margin-bottom: var(--space-2xl); 885 + padding-bottom: var(--space-xl); 886 + border-bottom: 1px solid var(--color-border); 887 + } 888 + 889 + footer { 890 + margin-top: var(--space-3xl); 891 + padding-top: var(--space-xl); 892 + border-top: 1px solid var(--color-border); 893 + font-size: var(--font-size-sm); 894 + color: var(--color-text-muted); 895 + } 896 + 897 + /** 898 + * Nav 899 + * Navigation menus 900 + */ 901 + nav { 902 + margin: var(--space-lg) 0; 903 + } 904 + 905 + nav ul { 906 + list-style: none; 907 + padding: 0; 908 + display: flex; 909 + gap: var(--space-md); 910 + flex-wrap: wrap; 911 + } 912 + 913 + nav li { 914 + margin: 0; 915 + } 916 + 917 + /** 918 + * Details and Summary 919 + * Disclosure widget for expandable content 920 + */ 921 + details { 922 + margin: var(--space-lg) 0; 923 + padding: var(--space-md); 924 + border: 1px solid var(--color-border); 925 + border-radius: var(--radius-md); 926 + max-width: var(--content-width); 927 + } 928 + 929 + summary { 930 + font-weight: 700; 931 + cursor: pointer; 932 + user-select: none; 933 + padding: var(--space-sm); 934 + margin: calc(-1 * var(--space-sm)); 935 + transition: background-color var(--transition-fast); 936 + } 937 + 938 + summary:hover { 939 + background-color: var(--color-bg-alt); 940 + } 941 + 942 + details[open] summary { 943 + margin-bottom: var(--space-md); 944 + border-bottom: 1px solid var(--color-border); 945 + } 946 + 947 + /* ========================================================================== 948 + Utility Classes (Minimal, for framework integration) 949 + ========================================================================== */ 950 + 951 + /** 952 + * Screen reader only 953 + * Hides content visually but keeps it accessible to assistive technology 954 + */ 955 + .sr-only { 956 + position: absolute; 957 + width: 1px; 958 + height: 1px; 959 + padding: 0; 960 + margin: -1px; 961 + overflow: hidden; 962 + clip: rect(0, 0, 0, 0); 963 + white-space: nowrap; 964 + border-width: 0; 965 + } 966 + 967 + /* ========================================================================== 968 + Print Styles 969 + ========================================================================== */ 970 + 971 + /** 972 + * Print-specific optimizations 973 + */ 974 + @media print { 975 + body { 976 + font-size: 12pt; 977 + line-height: 1.5; 978 + color: #000; 979 + background: #fff; 980 + } 981 + 982 + a { 983 + text-decoration: underline; 984 + color: #000; 985 + } 986 + 987 + /* Display URLs after links */ 988 + a[href^="http"]::after { 989 + content: " (" attr(href) ")"; 990 + font-size: 0.8em; 991 + } 992 + 993 + /* Hide sidenotes positioning, show inline */ 994 + @media (min-width: 768px) { 995 + p small { 996 + position: static; 997 + width: auto; 998 + left: auto; 999 + } 1000 + } 1001 + 1002 + /* Avoid page breaks inside elements */ 1003 + h1, h2, h3, h4, h5, h6, p, li, blockquote { 1004 + page-break-inside: avoid; 1005 + } 1006 + 1007 + /* Ensure images fit page */ 1008 + img { 1009 + max-width: 100% !important; 1010 + } 1011 + } 1012 + 1013 + /* ========================================================================== 1014 + Responsive Breakpoints 1015 + ========================================================================== */ 1016 + 1017 + /** 1018 + * Tablet and below - Reduce spacing 1019 + */ 1020 + @media (max-width: 768px) { 1021 + :root { 1022 + --font-size-base: 16px; 1023 + --space-2xl: 2rem; 1024 + --space-3xl: 3rem; 1025 + } 1026 + 1027 + body { 1028 + padding: var(--space-lg) var(--space-md); 1029 + } 1030 + 1031 + h1 { 1032 + font-size: var(--font-size-4xl); 1033 + } 1034 + 1035 + h2 { 1036 + font-size: var(--font-size-3xl); 1037 + } 1038 + 1039 + /* Stack navigation vertically */ 1040 + nav ul { 1041 + flex-direction: column; 1042 + gap: var(--space-sm); 1043 + } 1044 + } 1045 + 1046 + /** 1047 + * Mobile - Further reduced spacing and sizing 1048 + */ 1049 + @media (max-width: 480px) { 1050 + :root { 1051 + --font-size-base: 15px; 1052 + } 1053 + 1054 + body { 1055 + padding: var(--space-md) var(--space-sm); 1056 + } 1057 + 1058 + h1 { 1059 + font-size: var(--font-size-3xl); 1060 + } 1061 + 1062 + /* Reduce horizontal padding on smaller screens */ 1063 + pre { 1064 + padding: var(--space-md); 1065 + } 1066 + 1067 + table { 1068 + font-size: var(--font-size-sm); 1069 + } 1070 + }