From d308569f69b0356b808ad85b412debeca199c3af Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sat, 18 Oct 2025 00:48:28 -0500 Subject: [PATCH] feat: doc versioning & theming for docsite --- .gitignore | 2 + cli/src/commands/css-docs.ts | 26 +++--- cli/src/commands/docs.ts | 4 +- cli/src/versioning/differ.ts | 114 +++++++++++++++++++++++ cli/src/versioning/storage.ts | 73 +++++++++++++++ cli/src/versioning/tracker.ts | 107 +++++++++++++++++++++ docs/.versions.json | 158 ++++++++++++++++++++++++++++++++ docs/.vitepress/config.ts | 32 +++---- docs/.vitepress/theme/index.ts | 19 ++-- docs/.vitepress/theme/style.css | 149 +++++++++++++++--------------- docs/api/binder.md | 18 ++++ docs/api/dom.md | 67 ++++++++++++++ docs/api/evaluator.md | 29 ++++++ docs/api/signal.md | 70 ++++++++++++++ docs/css/semantics.md | 5 + 15 files changed, 755 insertions(+), 118 deletions(-) create mode 100644 cli/src/versioning/differ.ts create mode 100644 cli/src/versioning/storage.ts create mode 100644 cli/src/versioning/tracker.ts create mode 100644 docs/.versions.json create mode 100644 docs/api/binder.md create mode 100644 docs/api/dom.md create mode 100644 docs/api/evaluator.md create mode 100644 docs/api/signal.md diff --git a/.gitignore b/.gitignore index a547bf3..0d9e65f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +**/.vitepress/cache/ diff --git a/cli/src/commands/css-docs.ts b/cli/src/commands/css-docs.ts index 62ec2b8..7a1c9f7 100644 --- a/cli/src/commands/css-docs.ts +++ b/cli/src/commands/css-docs.ts @@ -1,14 +1,16 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { echo } from "../console/echo"; +import { trackVersion } from "../versioning/tracker.js"; 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 + * Extract CSS doc comments from CSS file by parsing block comments and associatint them with selectors */ function extractCSSComments(cssContent: string): CSSComment[] { const comments: CSSComment[] = []; @@ -71,8 +73,7 @@ function extractCSSComments(cssContent: string): CSSComment[] { } /** - * Extract CSS custom properties (variables) from :root - * Groups them by category based on naming conventions + * Extract CSS custom properties (variables) from :root, grouping them by category based on naming conventions */ function extractCSSVariables(cssContent: string): CSSVariable[] { const variables: CSSVariable[] = []; @@ -96,7 +97,7 @@ function extractCSSVariables(cssContent: string): CSSVariable[] { const match = trimmed.match(/^(--[a-z0-9-]+)\s*:\s*([^;]+);/); if (match) { const [, name, value] = match; - const category = categorizeCSSVariable(name); + const category = categorizeCSSVar(name); variables.push({ name, value: value.trim(), category }); } } @@ -108,7 +109,7 @@ function extractCSSVariables(cssContent: string): CSSVariable[] { /** * Categorize CSS variable by name prefix */ -function categorizeCSSVariable(name: string): string { +function categorizeCSSVar(name: string): string { if (name.startsWith("--font")) return "Typography"; if (name.startsWith("--line-height")) return "Typography"; if (name.startsWith("--space")) return "Spacing"; @@ -121,10 +122,6 @@ function categorizeCSSVariable(name: string): string { return "Other"; } -/** - * Validate CSS element coverage - * Checks which HTML elements have styling defined - */ function validateElementCoverage(cssContent: string): ElementCoverage[] { const elementsToCheck = [ "html", @@ -274,9 +271,6 @@ function generateSemanticsDocs(comments: CSSComment[], variables: CSSVariable[], return lines.join("\n"); } -/** - * Group HTML elements by category for better organization - */ function groupElementsByCategory(elements: string[]): Record { const categories: Record = { "Document Structure": [], @@ -365,6 +359,7 @@ function groupElementsByCategory(elements: string[]): Record { /** * CSS documentation command implementation + * * Generates semantics.md from base.css */ export async function cssDocsCommand(): Promise { @@ -399,7 +394,10 @@ export async function cssDocsCommand(): Promise { const markdown = generateSemanticsDocs(comments, variables, coverage); await mkdir(outputDir, { recursive: true }); - await writeFile(outputPath, markdown, "utf8"); + + echo.info("\nTracking version..."); + const versionedContent = await trackVersion(outputPath, markdown); + await writeFile(outputPath, versionedContent, "utf8"); echo.success(`\nCSS documentation generated: docs/css/semantics.md\n`); echo.label("Summary:"); diff --git a/cli/src/commands/docs.ts b/cli/src/commands/docs.ts index 77ca419..0aca734 100644 --- a/cli/src/commands/docs.ts +++ b/cli/src/commands/docs.ts @@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import ts from "typescript"; import { echo } from "../console/echo.js"; +import { trackVersion } from "../versioning/tracker.js"; type Member = { name: string; type: string; docs?: string }; @@ -253,7 +254,8 @@ async function processFile(filePath: string, baseDir: string, outputDir: string) const markdown = generateMD(entries, moduleName, moduleDocs); const outputPath = path.join(outputDir, `${moduleName}.md`); - await writeFile(outputPath, markdown, "utf8"); + const versionedContent = await trackVersion(outputPath, markdown); + await writeFile(outputPath, versionedContent, "utf8"); echo.ok(` Generated: ${relativePath} -> api/${moduleName}.md`); } diff --git a/cli/src/versioning/differ.ts b/cli/src/versioning/differ.ts new file mode 100644 index 0000000..0978674 --- /dev/null +++ b/cli/src/versioning/differ.ts @@ -0,0 +1,114 @@ +import { createHash } from "node:crypto"; + +/** + * Represents a section extracted from markdown + */ +export type Section = { heading: string; content: string; hash: string }; + +/** + * Result of diffing two sets of sections + */ +export type SectionDiff = { added: number; removed: number; edited: number }; + +/** + * Extract all ## and ### headings from markdown content + */ +export function extractSections(markdown: string): Section[] { + const lines = markdown.split("\n"); + const sections: Section[] = []; + let currentSection: { heading: string; lines: string[] } | undefined = undefined; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith("## ") || trimmed.startsWith("### ")) { + if (currentSection) { + const content = currentSection.lines.join("\n").trim(); + sections.push({ heading: currentSection.heading, content, hash: hashContent(content) }); + } + + currentSection = { heading: trimmed, lines: [] }; + } else if (currentSection) { + currentSection.lines.push(line); + } + } + + if (currentSection) { + const content = currentSection.lines.join("\n").trim(); + sections.push({ heading: currentSection.heading, content, hash: hashContent(content) }); + } + + return sections; +} + +/** + * Compare two sets of sections (matched by heading text) and calculate the diff + */ +export function diffSections(oldSections: Section[], newSections: Section[]): SectionDiff { + const oldMap = new Map(oldSections.map((s) => [s.heading, s])); + const newMap = new Map(newSections.map((s) => [s.heading, s])); + + let added = 0; + let removed = 0; + let edited = 0; + + for (const [heading, newSection] of newMap) { + const oldSection = oldMap.get(heading); + + if (!oldSection) { + added++; + } else if (oldSection.hash !== newSection.hash) { + edited++; + } + } + + // Find removed sections + for (const heading of oldMap.keys()) { + if (!newMap.has(heading)) { + removed++; + } + } + + return { added, removed, edited }; +} + +/** + * Hash content using SHA-256 + */ +function hashContent(content: string): string { + return createHash("sha256").update(content).digest("hex"); +} + +/** + * Extract section headings as a simple string array + */ +export function extractHeadings(markdown: string): string[] { + return extractSections(markdown).map((s) => s.heading); +} + +/** + * Hash entire markdown content (without frontmatter) + */ +export function hashMarkdown(markdown: string): string { + const withoutFrontmatter = stripFrontmatter(markdown); + return hashContent(withoutFrontmatter); +} + +/** + * Remove YAML frontmatter from markdown + */ +function stripFrontmatter(markdown: string): string { + const lines = markdown.split("\n"); + + if (lines[0]?.trim() !== "---") { + return markdown; + } + + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === "---") { + return lines.slice(i + 1).join("\n"); + } + } + + return markdown; +} diff --git a/cli/src/versioning/storage.ts b/cli/src/versioning/storage.ts new file mode 100644 index 0000000..84d577c --- /dev/null +++ b/cli/src/versioning/storage.ts @@ -0,0 +1,73 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +/** + * Version history entry for a single version + */ +export type VersionHistoryEntry = { + version: string; + date: string; + hash: string; + added: number; + removed: number; + edited: number; +}; + +/** + * Complete metadata for a single document + */ +export type DocMetadata = { + version: string; + updated: string; + hash: string; + sections: string[]; + history: VersionHistoryEntry[]; +}; + +/** + * Complete metadata storage for all documents by mapping relative file paths to their metadata + */ +export type VersionsMetadata = Record; + +const METADATA_PATH = path.join(process.cwd(), "..", "docs", ".versions.json"); + +/** + * Load the complete versions metadata from disk + */ +export async function loadMetadata(): Promise { + try { + const content = await readFile(METADATA_PATH, "utf8"); + return JSON.parse(content); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return {}; + } + throw error; + } +} + +/** + * Save the complete versions metadata to disk & creates the docs directory if it doesn't exist + */ +export async function saveMetadata(metadata: VersionsMetadata): Promise { + const docsDir = path.dirname(METADATA_PATH); + await mkdir(docsDir, { recursive: true }); + await writeFile(METADATA_PATH, JSON.stringify(metadata, null, 2), "utf8"); +} + +/** + * Get metadata for a specific document + */ +export async function getDocMetadata(relativePath: string): Promise { + const metadata = await loadMetadata(); + return metadata[relativePath]; +} + +/** + * Update metadata for a specific document or creates a new entry if document hasn't been versioned yet + */ +export async function updateDocMetadata(relativePath: string, docMeta: DocMetadata): Promise { + const metadata = await loadMetadata(); + metadata[relativePath] = docMeta; + await saveMetadata(metadata); +} diff --git a/cli/src/versioning/tracker.ts b/cli/src/versioning/tracker.ts new file mode 100644 index 0000000..5e8b816 --- /dev/null +++ b/cli/src/versioning/tracker.ts @@ -0,0 +1,107 @@ +import path from "node:path"; +import { diffSections, extractHeadings, extractSections, hashMarkdown } from "./differ.js"; +import type { DocMetadata } from "./storage.js"; +import { getDocMetadata, updateDocMetadata } from "./storage.js"; + +/** + * Track version for a generated documentation file + * Compares with previous version, calculates diff, bumps version, adds frontmatter + * + * @param filePath Absolute path to the documentation file + * @param content Generated markdown content (without frontmatter) + * @returns Content with frontmatter prepended + */ +export async function trackVersion(filePath: string, content: string): Promise { + const docsDir = path.join(process.cwd(), "..", "docs"); + const relPath = path.relative(docsDir, filePath); + + const prevMeta = await getDocMetadata(relPath); + + const newSections = extractSections(content); + const newHash = hashMarkdown(content); + const today = new Date().toISOString().split("T")[0]; + + if (!prevMeta) { + const initialMeta: DocMetadata = { + version: "1.0", + updated: today, + hash: newHash, + sections: extractHeadings(content), + history: [{ version: "1.0", date: today, hash: newHash, added: newSections.length, removed: 0, edited: 0 }], + }; + + await updateDocMetadata(relPath, initialMeta); + return addFrontmatter(content, "1.0", today); + } + + if (prevMeta.hash === newHash) { + return addFrontmatter(content, prevMeta.version, prevMeta.updated); + } + + const oldSections = extractSections(prevMeta.sections.map((h) => `${h}\n\nContent`).join("\n\n")); + const diff = diffSections(oldSections, newSections); + + const newVersion = calculateVersionBump(diff, prevMeta.version); + + const newMeta: DocMetadata = { + version: newVersion, + updated: today, + hash: newHash, + sections: extractHeadings(content), + history: [...prevMeta.history, { + version: newVersion, + date: today, + hash: newHash, + added: diff.added, + removed: diff.removed, + edited: diff.edited, + }], + }; + + await updateDocMetadata(relPath, newMeta); + return addFrontmatter(content, newVersion, today); +} + +/** + * Calculate version bump based on diff + * + * Rules: + * - Any sections removed → Major bump + * - Any sections added → Major bump + * - ≥4 sections edited → Major bump + * - 1-3 sections edited → Minor bump + * - No changes → No bump + */ +function calculateVersionBump(diff: { added: number; removed: number; edited: number }, current: string): string { + const [major, minor] = current.split(".").map(Number); + + if (diff.removed > 0) { + return `${major + 1}.0`; + } + + if (diff.added > 0) { + return `${major + 1}.0`; + } + + if (diff.edited >= 4) { + return `${major + 1}.0`; + } + + if (diff.edited > 0) { + return `${major}.${minor + 1}`; + } + + return current; +} + +/** + * Add frontmatter to markdown content + */ +function addFrontmatter(content: string, version: string, date: string): string { + return `--- +version: ${version} +updated: ${date} +--- + +${content}`; +} diff --git a/docs/.versions.json b/docs/.versions.json new file mode 100644 index 0000000..6625a7f --- /dev/null +++ b/docs/.versions.json @@ -0,0 +1,158 @@ +{ + "css/semantics.md": { + "version": "1.0", + "updated": "2025-10-18", + "hash": "5771ebe5cced18431c542d5ac24f2e483ae0a2cd6368d3de30e62cbc17b05326", + "sections": [ + "## CSS Custom Properties", + "### Typography", + "### Spacing", + "### Layout", + "### Colors", + "### Effects", + "## Element Coverage", + "### Styled Elements", + "### Unstyled Elements", + "## Documentation Comments", + "### `:root`", + "### `@media (prefers-color-scheme: dark)`", + "### `*, *::before, *::after`", + "### `html`", + "### `body`", + "### `h1, h2, h3, h4, h5, h6`", + "### `h1`", + "### `p`", + "### `h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p`", + "### `a`", + "### `em`", + "### `mark`", + "### `sub, sup`", + "### `small`", + "### `ul, ol`", + "### `li`", + "### `li > ul, li > ol`", + "### `dl`", + "### `p:has(small)`", + "### `p small`", + "### `@media (max-width: 767px)`", + "### `blockquote`", + "### `cite`", + "### `code`", + "### `kbd`", + "### `samp`", + "### `var`", + "### `pre`", + "### `hr`", + "### `table`", + "### `thead`", + "### `td`", + "### `tbody tr:nth-child(even)`", + "### `tbody tr:hover`", + "### `form`", + "### `fieldset`", + "### `label`", + "### `textarea`", + "### `input[type=\"checkbox\"],`", + "### `input[type=\"file\"]`", + "### `input[type=\"range\"]`", + "### `progress, meter`", + "### `input[type=\"reset\"]`", + "### `img`", + "### `figure`", + "### `video, audio`", + "### `canvas, svg`", + "### `iframe`", + "### `article, section`", + "### `aside`", + "### `header`", + "### `nav`", + "### `details`", + "### `.sr-only`", + "### `@media print`", + "### `@media (max-width: 768px)`", + "### `@media (max-width: 480px)`" + ], + "history": [ + { + "version": "1.0", + "date": "2025-10-18", + "hash": "5771ebe5cced18431c542d5ac24f2e483ae0a2cd6368d3de30e62cbc17b05326", + "added": 67, + "removed": 0, + "edited": 0 + } + ] + }, + "api/binder.md": { + "version": "1.0", + "updated": "2025-10-18", + "hash": "be8aac3c45149a3777d89f58e232935acb3720e6aa36f697eb11e58b416e0d09", + "sections": ["## mount"], + "history": [ + { + "version": "1.0", + "date": "2025-10-18", + "hash": "be8aac3c45149a3777d89f58e232935acb3720e6aa36f697eb11e58b416e0d09", + "added": 1, + "removed": 0, + "edited": 0 + } + ] + }, + "api/dom.md": { + "version": "1.0", + "updated": "2025-10-18", + "hash": "72a59e5fa86de7b102089a7f84ff3d7c7674ab025dd75a4fb19264e50a7e32ab", + "sections": [ + "## walkDOM", + "## hasVoltAttribute", + "## getVoltAttributes", + "## setText", + "## setHTML", + "## toggleClass", + "## parseClassBinding" + ], + "history": [ + { + "version": "1.0", + "date": "2025-10-18", + "hash": "72a59e5fa86de7b102089a7f84ff3d7c7674ab025dd75a4fb19264e50a7e32ab", + "added": 7, + "removed": 0, + "edited": 0 + } + ] + }, + "api/evaluator.md": { + "version": "1.0", + "updated": "2025-10-18", + "hash": "fc8aa3ad16b62bdb2d16f86a476070a1acacb944b407b8c5edd033cb44fbf7f4", + "sections": ["## Scope", "## evaluate"], + "history": [ + { + "version": "1.0", + "date": "2025-10-18", + "hash": "fc8aa3ad16b62bdb2d16f86a476070a1acacb944b407b8c5edd033cb44fbf7f4", + "added": 2, + "removed": 0, + "edited": 0 + } + ] + }, + "api/signal.md": { + "version": "1.0", + "updated": "2025-10-18", + "hash": "fcd3af42730ec609900fb5d1355b100ef3ab2506d9dd00396d6a582a363409e4", + "sections": ["## Signal", "## ComputedSignal", "## signal", "## computed", "## effect"], + "history": [ + { + "version": "1.0", + "date": "2025-10-18", + "hash": "fcd3af42730ec609900fb5d1355b100ef3ab2506d9dd00396d6a582a363409e4", + "added": 5, + "removed": 0, + "edited": 0 + } + ] + } +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3306b6d..190be62 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,28 +1,22 @@ -import { defineConfig } from 'vitepress' +import { defineConfig } from "vitepress"; // https://vitepress.dev/reference/site-config export default defineConfig({ title: "Volt.js", description: "A reactive, hypermedia framework.", + appearance: "dark", themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: 'Home', link: '/' }, - { text: 'Examples', link: '/markdown-examples' } - ], - + nav: [{ text: "Home", link: "/" }, { text: "CSS", link: "/css/volt-css" }, { text: "API", link: "/api-examples" }], sidebar: [ + { text: "Getting Started", items: [{ text: "Introduction", link: "/" }] }, { - text: 'Examples', - items: [ - { text: 'Markdown Examples', link: '/markdown-examples' }, - { text: 'Runtime API Examples', link: '/api-examples' } - ] - } + text: "CSS", + collapsed: false, + items: [{ text: "Volt CSS", link: "/css/volt-css" }, { text: "CSS Reference", link: "/css/semantics" }], + }, + { text: "API Reference", collapsed: false, items: [{ text: "Runtime API", link: "/api-examples" }] }, + { text: "Examples", collapsed: true, items: [{ text: "Markdown Examples", link: "/markdown-examples" }] }, ], - - socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' } - ] - } -}) + socialLinks: [{ icon: "github", link: "https://github.com/stormlightlabs/volt" }], + }, +}); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index def4cfc..0fdde3d 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,17 +1,14 @@ // https://vitepress.dev/guide/custom-theme -import { h } from 'vue' -import type { Theme } from 'vitepress' -import DefaultTheme from 'vitepress/theme' -import './style.css' +import type { Theme } from "vitepress"; +import DefaultTheme from "vitepress/theme"; +import { h } from "vue"; +import "./style.css"; export default { extends: DefaultTheme, + // https://vitepress.dev/guide/extending-default-theme#layout-slots Layout: () => { - return h(DefaultTheme.Layout, null, { - // https://vitepress.dev/guide/extending-default-theme#layout-slots - }) + return h(DefaultTheme.Layout, null, {}); }, - enhanceApp({ app, router, siteData }) { - // ... - } -} satisfies Theme + enhanceApp({ app, router, siteData }) {}, +} satisfies Theme; diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 1a61cb1..7c1835b 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -4,76 +4,82 @@ */ /** - * Colors - * - * Each colors have exact same color scale system with 3 levels of solid - * colors with different brightness, and 1 soft color. - * - * - `XXX-1`: The most solid color used mainly for colored text. It must - * satisfy the contrast ratio against when used on top of `XXX-soft`. - * - * - `XXX-2`: The color used mainly for hover state of the button. - * - * - `XXX-3`: The color for solid background, such as bg color of the button. - * It must satisfy the contrast ratio with pure white (#ffffff) text on - * top of it. - * - * - `XXX-soft`: The color used for subtle background such as custom container - * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors - * on top of it. - * - * The soft color must be semi transparent alpha channel. This is crucial - * because it allows adding multiple "soft" colors on top of each other - * to create a accent, such as when having inline code block inside - * custom containers. - * - * - `default`: The color used purely for subtle indication without any - * special meanings attached to it such as bg color for menu hover state. - * - * - `brand`: Used for primary brand colors, such as link text, button with - * brand theme, etc. - * - * - `tip`: Used to indicate useful information. The default theme uses the - * brand color for this by default. - * - * - `warning`: Used to indicate warning to the users. Used in custom - * container, badges, etc. - * - * - `danger`: Used to show error, or dangerous message to the users. Used - * in custom container, badges, etc. - * -------------------------------------------------------------------------- */ + * Seattle SuperSonics Color Palette + * - Emerald Green (#173F35) - Primary + * - Red (#9E2A2F) - Danger + * - Yellow (#FFA300) - Secondary/Warning + * - Bronze (#8B634B) - Accent + **/ +:root { + /* SuperSonics Emerald Green - Primary Brand */ + --vp-c-sonics-green-1: #2a6b5d; + --vp-c-sonics-green-2: #1f5248; + --vp-c-sonics-green-3: #173F35; + --vp-c-sonics-green-soft: rgba(23, 63, 53, 0.14); + + /* SuperSonics Yellow - Secondary/Warning */ + --vp-c-sonics-yellow-1: #ffbb33; + --vp-c-sonics-yellow-2: #ffad1a; + --vp-c-sonics-yellow-3: #FFA300; + --vp-c-sonics-yellow-soft: rgba(255, 163, 0, 0.14); + + /* SuperSonics Red - Danger */ + --vp-c-sonics-red-1: #c24348; + --vp-c-sonics-red-2: #b03338; + --vp-c-sonics-red-3: #9E2A2F; + --vp-c-sonics-red-soft: rgba(158, 42, 47, 0.14); + + /* SuperSonics Bronze - Accent */ + --vp-c-sonics-bronze-1: #a3836b; + --vp-c-sonics-bronze-2: #97735a; + --vp-c-sonics-bronze-3: #8B634B; + --vp-c-sonics-bronze-soft: rgba(139, 99, 75, 0.14); +} +/** + * Dark Theme Colors + **/ :root { --vp-c-default-1: var(--vp-c-gray-1); --vp-c-default-2: var(--vp-c-gray-2); --vp-c-default-3: var(--vp-c-gray-3); --vp-c-default-soft: var(--vp-c-gray-soft); - --vp-c-brand-1: var(--vp-c-indigo-1); - --vp-c-brand-2: var(--vp-c-indigo-2); - --vp-c-brand-3: var(--vp-c-indigo-3); - --vp-c-brand-soft: var(--vp-c-indigo-soft); - - --vp-c-tip-1: var(--vp-c-brand-1); - --vp-c-tip-2: var(--vp-c-brand-2); - --vp-c-tip-3: var(--vp-c-brand-3); - --vp-c-tip-soft: var(--vp-c-brand-soft); - - --vp-c-warning-1: var(--vp-c-yellow-1); - --vp-c-warning-2: var(--vp-c-yellow-2); - --vp-c-warning-3: var(--vp-c-yellow-3); - --vp-c-warning-soft: var(--vp-c-yellow-soft); - - --vp-c-danger-1: var(--vp-c-red-1); - --vp-c-danger-2: var(--vp-c-red-2); - --vp-c-danger-3: var(--vp-c-red-3); - --vp-c-danger-soft: var(--vp-c-red-soft); + /* Use Emerald Green as primary brand color */ + --vp-c-brand-1: var(--vp-c-sonics-green-1); + --vp-c-brand-2: var(--vp-c-sonics-green-2); + --vp-c-brand-3: var(--vp-c-sonics-green-3); + --vp-c-brand-soft: var(--vp-c-sonics-green-soft); + + /* Tips use Bronze accent */ + --vp-c-tip-1: var(--vp-c-sonics-bronze-1); + --vp-c-tip-2: var(--vp-c-sonics-bronze-2); + --vp-c-tip-3: var(--vp-c-sonics-bronze-3); + --vp-c-tip-soft: var(--vp-c-sonics-bronze-soft); + + /* Warnings use Yellow */ + --vp-c-warning-1: var(--vp-c-sonics-yellow-1); + --vp-c-warning-2: var(--vp-c-sonics-yellow-2); + --vp-c-warning-3: var(--vp-c-sonics-yellow-3); + --vp-c-warning-soft: var(--vp-c-sonics-yellow-soft); + + /* Danger uses Red */ + --vp-c-danger-1: var(--vp-c-sonics-red-1); + --vp-c-danger-2: var(--vp-c-sonics-red-2); + --vp-c-danger-3: var(--vp-c-sonics-red-3); + --vp-c-danger-soft: var(--vp-c-sonics-red-soft); } -/** - * Component: Button - * -------------------------------------------------------------------------- */ +.dark { + /* Enhance dark mode background */ + --vp-c-bg: #0d1117; + --vp-c-bg-soft: #161b22; + --vp-c-bg-mute: #1c2128; +} +/** + * Button + **/ :root { --vp-button-brand-border: transparent; --vp-button-brand-text: var(--vp-c-white); @@ -87,21 +93,20 @@ } /** - * Component: Home - * -------------------------------------------------------------------------- */ - + * Home + **/ :root { --vp-home-hero-name-color: transparent; --vp-home-hero-name-background: -webkit-linear-gradient( 120deg, - #bd34fe 30%, - #41d1ff + #FFA300 30%, + #2a6b5d ); --vp-home-hero-image-background-image: linear-gradient( -45deg, - #bd34fe 50%, - #47caff 50% + #FFA300 50%, + #173F35 50% ); --vp-home-hero-image-filter: blur(44px); } @@ -119,9 +124,8 @@ } /** - * Component: Custom Block - * -------------------------------------------------------------------------- */ - + * Custom Block + **/ :root { --vp-custom-block-tip-border: transparent; --vp-custom-block-tip-text: var(--vp-c-text-1); @@ -130,9 +134,8 @@ } /** - * Component: Algolia - * -------------------------------------------------------------------------- */ - + * Algolia + **/ .DocSearch { --docsearch-primary-color: var(--vp-c-brand-1) !important; } diff --git a/docs/api/binder.md b/docs/api/binder.md new file mode 100644 index 0000000..7f5bb9a --- /dev/null +++ b/docs/api/binder.md @@ -0,0 +1,18 @@ +--- +version: 1.0 +updated: 2025-10-18 +--- + +# binder + +Binder system for mounting and managing Volt.js bindings + +## mount + +Mount Volt.js on a root element and its descendants. +Binds all data-x-* attributes to the provided scope. +Returns a cleanup function to unmount and dispose all bindings. + +```typescript +export function mount(root: Element, scope: Scope): CleanupFunction +``` diff --git a/docs/api/dom.md b/docs/api/dom.md new file mode 100644 index 0000000..91d0dd7 --- /dev/null +++ b/docs/api/dom.md @@ -0,0 +1,67 @@ +--- +version: 1.0 +updated: 2025-10-18 +--- + +# dom + +DOM utility functions + +## walkDOM + +Walk the DOM tree and collect all elements with data-x-* attributes. +Returns elements in document order (parent before children). + +```typescript +export function walkDOM(root: Element): Element[] +``` + +## hasVoltAttribute + +Check if an element has any data-x-* attributes. + +```typescript +export function hasVoltAttribute(element: Element): boolean +``` + +## getVoltAttributes + +Get all data-x-* attributes from an element. + +```typescript +export function getVoltAttributes(element: Element): Map +``` + +## setText + +Set the text content of an element safely. + +```typescript +export function setText(element: Element, value: unknown): void +``` + +## setHTML + +Set the HTML content of an element safely. +Note: This trusts the input HTML and should only be used with sanitized content. + +```typescript +export function setHTML(element: Element, value: string): void +``` + +## toggleClass + +Add or remove a CSS class from an element. + +```typescript +export function toggleClass(element: Element, className: string, add: boolean): void +``` + +## parseClassBinding + +Parse a class binding expression. +Supports both string values ("active") and object notation ({active: true}). + +```typescript +export function parseClassBinding(value: unknown): Map +``` diff --git a/docs/api/evaluator.md b/docs/api/evaluator.md new file mode 100644 index 0000000..720e838 --- /dev/null +++ b/docs/api/evaluator.md @@ -0,0 +1,29 @@ +--- +version: 1.0 +updated: 2025-10-18 +--- + +# evaluator + +Safe expression evaluation of simple expressions without using eval() for bindings + +## Scope + +Safe expression evaluation of simple expressions without using eval() for bindings + +```typescript +Record +``` + +## evaluate + +Evaluate a simple expression against a scope object. +Supports: +- Property access: "count", "user.name", "items.length" +- Simple literals: "true", "false", "null", "undefined" +- Numbers: "42", "3.14" +- Strings: "'hello'", '"world"' + +```typescript +export function evaluate(expression: string, scope: Scope): unknown +``` diff --git a/docs/api/signal.md b/docs/api/signal.md new file mode 100644 index 0000000..7de14a6 --- /dev/null +++ b/docs/api/signal.md @@ -0,0 +1,70 @@ +--- +version: 1.0 +updated: 2025-10-18 +--- + +# signal + +A reactive primitive that notifies subscribers when its value changes. + +## Signal + +A reactive primitive that notifies subscribers when its value changes. + +## ComputedSignal + +A computed signal that derives its value from other signals. + +## signal + +Creates a new signal with the given initial value. +Signals are reactive primitives that automatically notify subscribers when changed. + +```typescript +export function signal(initialValue: T): Signal +``` + +**Example:** + +```typescript +const count = signal(0); +count.subscribe(value => console.log('Count:', value)); +count.set(1); // Logs: Count: 1 +``` + +## computed + +Creates a computed signal that derives its value from other signals. +The computation function is re-run whenever any of its dependencies change. + +```typescript +export function computed( compute: () => T, dependencies: Array | ComputedSignal>, ): ComputedSignal +``` + +**Example:** + +```typescript +const count = signal(5); +const doubled = computed(() => count.get() * 2, [count]); +doubled.get(); // 10 +count.set(10); +doubled.get(); // 20 +``` + +## effect + +Creates a side effect that runs when dependencies change. +Effects run immediately on creation and whenever dependencies update. + +```typescript +export function effect( effectFunction: () => void | (() => void), dependencies: Array | ComputedSignal>, ): () => void +``` + +**Example:** + +```typescript +const count = signal(0); +const cleanup = effect(() => { + console.log('Count changed:', count.get()); +}, [count]); +``` diff --git a/docs/css/semantics.md b/docs/css/semantics.md index f71836f..709db8a 100644 --- a/docs/css/semantics.md +++ b/docs/css/semantics.md @@ -1,3 +1,8 @@ +--- +version: 1.0 +updated: 2025-10-18 +--- + # Volt CSS Semantics Auto-generated documentation from base.css -- 2.51.2