diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0a6a0c95 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: true +contact_links: + - name: Contributing overview + url: https://code.textmode.art/docs/contributing/ + about: Choose the correct repository and contribution path before opening an issue. + - name: Documentation + url: https://code.textmode.art/ + about: Search the guides, API reference, and examples before reporting a documentation problem. + - name: textmode.js core repository + url: https://github.com/humanbydefinition/textmode.js/issues + about: Report core library and rendering problems in the core repository. + - name: Discord + url: https://discord.gg/sjrw8QXNks + about: Ask usage questions or discuss documentation ideas before filing an issue. diff --git a/.github/assets/readme-og.options.json b/.github/assets/readme-og.options.json new file mode 100644 index 00000000..20688e47 --- /dev/null +++ b/.github/assets/readme-og.options.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "generator": { + "package": "@textmode/og", + "version": "0.1.0" + }, + "source": { + "name": "textmodeshift", + "repository": "https://github.com/humanbydefinition/editor.textmode.art", + "revision": "b037294d3e0abeff074d50b41235015b3f7d8e67", + "path": "sketches/textmodeshift/sketch.js", + "variantSeed": "readme:code.textmode.art:v1", + "randomSeedTemplate": "textmodeshift-v1:${variantSeed}:${seed}" + }, + "output": { + "path": "readme-og.png", + "layout": { + "kind": "main" + }, + "frame": 60, + "darken": 70 + }, + "branding": { + "logo": "default", + "labels": { + "brandName": "code.textmode.art", + "mainTopRight": "OFFICIAL DOCUMENTATION", + "mainHeadlinePrefix": "EXPLORE", + "mainHeadlineEmphasis": "TEXTMODE", + "mainHeadlineSecondLine": "ON THE WEB", + "mainBottomLeft": "GUIDES / API / EXAMPLES / INTEGRATIONS", + "mainBottomRight": "TEXTMODE.ART" + } + } +} diff --git a/.github/assets/readme-og.png b/.github/assets/readme-og.png new file mode 100644 index 00000000..c529b899 Binary files /dev/null and b/.github/assets/readme-og.png differ diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 5ab80ed6..cf04153a 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -43,10 +43,19 @@ jobs: exit 1 - name: Checkout canonical source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v6 with: persist-credentials: false + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + + - name: Report toolchain + run: node --version && npm --version + - name: Validate registry, tests, and generated README run: npm run check:contributors diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 37c41b07..21247324 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,27 +19,30 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version-file: .nvmrc cache: npm + - name: Report toolchain + run: node --version && npm --version + - name: Setup Pages - uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4 + uses: actions/configure-pages@v4 - name: Install dependencies - run: npm install --frozen-lockfile + run: npm ci - name: Build VitePress site run: npm run build - name: Upload artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + uses: actions/upload-pages-artifact@v3 with: path: .vitepress/dist @@ -53,4 +56,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/reconcile-contributors.yml b/.github/workflows/reconcile-contributors.yml index 2d14773d..ff380ad1 100644 --- a/.github/workflows/reconcile-contributors.yml +++ b/.github/workflows/reconcile-contributors.yml @@ -66,7 +66,7 @@ jobs: - name: Create short-lived target token id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + uses: actions/create-github-app-token@v3 with: client-id: ${{ inputs.app-client-id }} private-key: ${{ secrets.app-private-key }} @@ -82,7 +82,7 @@ jobs: run: gh api "repos/humanbydefinition/$TARGET_REPOSITORY/branches/dev" --silent - name: Checkout target dev branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v6 with: repository: humanbydefinition/${{ inputs.target-repository }} ref: dev @@ -91,11 +91,12 @@ jobs: token: ${{ steps.app-token.outputs.token }} - name: Checkout canonical contributor source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v6 with: repository: humanbydefinition/code.textmode.art ref: ${{ inputs.source-ref }} sparse-checkout: | + .nvmrc .vitepress/data/contribution-types.json .vitepress/data/contributors.json scripts/contributors.mjs @@ -103,6 +104,14 @@ jobs: path: contributor-source persist-credentials: false + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: contributor-source/.nvmrc + + - name: Report toolchain + run: node --version && npm --version + - name: Resolve canonical source id: source working-directory: contributor-source diff --git a/.gitignore b/.gitignore index 3b3198a6..ed455c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules .vitepress/dist/ .vitepress/temp/ .agents -skills-lock.json \ No newline at end of file +skills-lock.json +.vitepress/data/.generated-editor-sketches.json \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.vitepress/configs/head.mts b/.vitepress/configs/head.mts index c43f48ae..b4c6123c 100644 --- a/.vitepress/configs/head.mts +++ b/.vitepress/configs/head.mts @@ -54,7 +54,7 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { const defaultDescription = 'textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.' const ogTitle = pageData.title || 'textmode.js' const ogDescription = pageData.description || defaultDescription - const ogImage = 'https://code.textmode.art/png/textmodejs_banner.png' + const ogImage = 'https://code.textmode.art/png/readme-og.png' // Detect page types const isBlogPost = pageData.relativePath.startsWith('blog/') && pageData.relativePath !== 'blog/index.md'; diff --git a/.vitepress/configs/sidebar.mts b/.vitepress/configs/sidebar.mts index 138e0747..f2e7f904 100644 --- a/.vitepress/configs/sidebar.mts +++ b/.vitepress/configs/sidebar.mts @@ -45,9 +45,10 @@ const docsSidebar: DefaultTheme.SidebarItem[] = [ link: "/docs/randomness-math-vectors-and-noise", }, { text: "Transforms", link: "/docs/transforms" }, - { text: "Input events", link: "/docs/events" }, - { text: "Fonts and tilesets", link: "/docs/fonts" }, - { text: "Media sources", link: "/docs/loadables" }, + { text: "Input events", link: "/docs/event-handling" }, + { text: "Fonts and tilesets", link: "/docs/fonts-and-tilesets" }, + { text: "FIGlet typography", link: "/docs/figlet-typography" }, + { text: "Media sources", link: "/docs/media-sources" }, { text: "Media conversion", link: "/docs/media-conversion" }, ], }, @@ -86,8 +87,8 @@ const docsSidebar: DefaultTheme.SidebarItem[] = [ items: [ { text: "flok.cc", link: "/docs/live-coding-flok-cc" }, { - text: "synth.textmode.art", - link: "/docs/live-coding-synth-textmode-art", + text: "editor.textmode.art", + link: "/docs/live-coding-editor-textmode-art", }, ], }, @@ -97,15 +98,15 @@ const docsSidebar: DefaultTheme.SidebarItem[] = [ text: "Contributing", items: [ { text: "Overview", link: "/docs/contributing/" }, + { text: "Contribute code", link: "/docs/contributing/code" }, { - text: "Getting started", - link: "/docs/contributing/getting-started", + text: "Contribute docs", + link: "/docs/contributing/docs", }, { text: "Submit a sketch", link: "/docs/contributing/submit-a-sketch", }, - { text: "Improve docs", link: "/docs/contributing/improve-docs" }, { text: "Contributors", link: "/docs/contributors" }, ], }, diff --git a/.vitepress/data/apiExamples.data.ts b/.vitepress/data/apiExamples.data.ts new file mode 100644 index 00000000..f2729222 --- /dev/null +++ b/.vitepress/data/apiExamples.data.ts @@ -0,0 +1,51 @@ +import { scanApiExamples } from '../../scripts/lib/api-examples-scan.mjs' + +export type ApiProfile = + | 'textmode.js' + | 'textmode.synth.js' + | 'textmode.filters.js' + | 'textmode.figlet.js' + | 'textmode.export.js' + +export interface ApiExampleSketch { + id: string + slug: string + profile: ApiProfile + language: 'javascript' | 'typescript' + encodedCode: string + apiPath: string + pageUrl: string + heading?: string + ordinal: number + title: string + description?: string + kind?: string + owner?: string + ecosystem?: string + category?: string + lastModified?: string + tags: string[] +} + +declare const data: ApiExampleSketch[] +export { data } + +/** + * Build-time catalog of every runnable API example sketch embedded across the + * generated API reference (the api directory). Each entry is one live sandbox + * instance, ready to be passed straight to . + * + * The API markdown is the single source of truth; this loader only *reads* it, + * so regenerating API docs automatically updates the Examples page catalog. + */ +export default { + watch: ['api/**/*.md'], + load(): ApiExampleSketch[] { + try { + return scanApiExamples() as ApiExampleSketch[] + } catch (error) { + console.warn('[apiExamples] Failed to build the API example catalog:', error) + return [] + } + }, +} diff --git a/.vitepress/data/apiSketches.data.ts b/.vitepress/data/apiSketches.data.ts deleted file mode 100644 index 162fae87..00000000 --- a/.vitepress/data/apiSketches.data.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { readFileSync, readdirSync, existsSync } from 'node:fs' -import { resolve, join, relative } from 'node:path' - -export interface ApiSketch { - title: string - author: string -} - -export type ApiSketchMap = Record - -declare const data: ApiSketchMap -export { data } - -function collectMarkdownFiles(dir: string): string[] { - const files: string[] = [] - if (!existsSync(dir)) return files - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name) - if (entry.isDirectory()) files.push(...collectMarkdownFiles(full)) - else if (entry.name.endsWith('.md')) files.push(full) - } - - return files -} - -export default { - watch: ['api/**/*.md'], - load(): ApiSketchMap { - const root = process.cwd() - const apiDir = resolve(root, 'api') - const files = collectMarkdownFiles(apiDir) - const sketches: ApiSketchMap = {} - - for (const file of files) { - const content = readFileSync(file, 'utf-8') - const lines = content.split('\n') - - const frontmatterMatch = content.match(/^---\s*\n[\s\S]*?title:\s*(.+)\n[\s\S]*?---/m) - const pageTitle = frontmatterMatch?.[1]?.trim() ?? '' - - let currentHeading = '' - let blockIndex = 0 - - for (let i = 0; i < lines.length; i++) { - const headingMatch = lines[i].match(/^###\s+(.+)/) - if (headingMatch) { - currentHeading = headingMatch[1].trim() - } - - if (!lines[i].includes('github.com/') || !lines[i].includes('.png')) continue - - const block = lines.slice(i, Math.min(i + 12, lines.length)).join('\n') - - const authorMatch = block.match(/]*>@([^<]+)<\/a><\/strong>/) - if (!authorMatch) continue - - const author = authorMatch[1] - const title = currentHeading - ? `${pageTitle} – ${currentHeading}` - : pageTitle - - const relPath = relative(root, file).replace(/[\\/]/g, '-').replace(/\.md$/, '') - const key = `api:${relPath}:${blockIndex++}` - - sketches[key] = { title, author } - } - } - - return sketches - }, -} diff --git a/.vitepress/data/editorSketches.data.ts b/.vitepress/data/editorSketches.data.ts new file mode 100644 index 00000000..bee0240f --- /dev/null +++ b/.vitepress/data/editorSketches.data.ts @@ -0,0 +1,122 @@ +export interface EditorSketchMeta { + slug: string + title: string + description?: string + authorName?: string + license?: string + socialLinks?: Array<{ label: string; url: string }> + createdAt?: string + ogFrame?: number + ogDarken?: number + featured?: boolean +} + +export interface EditorGallerySketch extends EditorSketchMeta { + textmodeCode: string + ogImageUrl?: string +} + +export type EditorSketchMap = Record + +declare const data: EditorGallerySketch[] +export { data } + +/** + * Refactors sketch code from editor.textmode.art for code.textmode.art Sandpack: + * 1. Ensures textmode.create is initialized with responsive dimensions: + * textmode.create({ width: window.innerWidth, height: window.innerHeight }) + * 2. Ensures a t.windowResized callback exists and invokes: + * t.resizeCanvas(window.innerWidth, window.innerHeight) + */ +export function refactorSketchCode(code: string): string { + let refactored = code.trim() + + // 1. Ensure textmode.create is initialized with responsive sizing + if ( + refactored.includes('const t = textmode.create') || + refactored.includes('let t = textmode.create') || + refactored.includes('var t = textmode.create') + ) { + refactored = refactored.replace( + /(?:const|let|var)\s+t\s*=\s*textmode\.create\s*\(\s*(?:\{\s*\})?\s*\)/g, + 'const t = textmode.create({ width: window.innerWidth, height: window.innerHeight })' + ) + } else { + refactored = `const t = textmode.create({ width: window.innerWidth, height: window.innerHeight });\n\n${refactored}` + } + + // 2. Ensure t.windowResized callback is present and calls t.resizeCanvas(window.innerWidth, window.innerHeight) + const hasWindowResized = refactored.includes('windowResized') + const hasResizeCanvas = refactored.includes('resizeCanvas') + + if (!hasWindowResized) { + refactored += `\n\nt.windowResized(() => {\n t.resizeCanvas(window.innerWidth, window.innerHeight);\n});\n` + } else if (!hasResizeCanvas) { + refactored = refactored.replace( + /(t\.windowResized\s*\(\s*(?:async\s*)?(?:function\s*\w*\s*\([^)]*\)|\([^)]*\)|[a-zA-Z0-9_$]+)?\s*(?:=>)?\s*\{)/g, + '$1\n t.resizeCanvas(window.innerWidth, window.innerHeight);' + ) + } + + return refactored +} + +const GITHUB_REPO_OWNER = 'humanbydefinition' +const GITHUB_REPO_NAME = 'editor.textmode.art' +const GITHUB_BRANCH = 'main' + +async function fetchFromGitHub(): Promise { + try { + const apiUrl = `https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/contents/sketches` + const headers: Record = { + 'User-Agent': 'code.textmode.art-build-loader', + Accept: 'application/vnd.github.v3+json', + } + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}` + } + + const res = await fetch(apiUrl, { headers }) + if (!res.ok) { + console.warn(`[editorSketches loader] GitHub API failed (${res.status} ${res.statusText}). Returning empty sketches.`) + return [] + } + + const items = (await res.json()) as Array<{ name: string; type: string }> + const sketchFolders = items.filter((item) => item.type === 'dir').map((item) => item.name) + + const sketches: EditorGallerySketch[] = [] + + for (const slug of sketchFolders) { + const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/${GITHUB_BRANCH}/sketches/${slug}` + const metaRes = await fetch(`${rawBase}/meta.json`, { headers }) + const codeRes = await fetch(`${rawBase}/sketch.js`, { headers }) + + if (!metaRes.ok || !codeRes.ok) continue + + const meta = (await metaRes.json()) as EditorSketchMeta + const rawCode = await codeRes.text() + const textmodeCode = refactorSketchCode(rawCode) + const ogImageUrl = `${rawBase}/og.png` + + sketches.push({ + ...meta, + slug: meta.slug || slug, + featured: true, + textmodeCode, + ogImageUrl, + }) + } + + return sketches.sort((a, b) => a.slug.localeCompare(b.slug)) + } catch (error) { + console.warn('[editorSketches loader] Exception fetching from GitHub:', error) + return [] + } +} + +export default { + async load(): Promise { + return await fetchFromGitHub() + }, +} diff --git a/.vitepress/data/gallery.json b/.vitepress/data/gallery.json index 4751689b..61424ea1 100644 --- a/.vitepress/data/gallery.json +++ b/.vitepress/data/gallery.json @@ -35,12 +35,12 @@ "featured": true, "date": null }, - "synth-textmode-art": { - "title": "synth.textmode.art", - "description": "Live coding environment for ASCII synthesis and algorithmic audio patterns.", + "editor-textmode-art": { + "title": "editor.textmode.art", + "description": "Browser-based live coding environment for textmode.js sketches, add-ons, and ASCII synthesis.", "type": "website", - "thumbnailUrl": "/gallery/synth-textmode-art.png", - "url": "https://synth.textmode.art/", + "thumbnailUrl": "/gallery/editor-textmode-art.png", + "url": "https://editor.textmode.art/", "author": "humanbydefinition", "authorUrl": "https://github.com/humanbydefinition", "tags": ["live-coding", "audio-reactive", "visuals"], diff --git a/.vitepress/data/sketches.json b/.vitepress/data/sketches.json deleted file mode 100644 index b4b0a644..00000000 --- a/.vitepress/data/sketches.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "wave-interference": { - "title": "Animated wave pattern", - "author": "humanbydefinition", - "featured": true - }, - "digital-rain": { - "title": "Digital rain", - "author": "humanbydefinition", - "featured": true - }, - "plasma-field": { - "title": "Plasma field", - "author": "humanbydefinition", - "featured": true - }, - "webcam": { - "title": "Webcam Integration", - "author": "humanbydefinition", - "featured": false - }, - "video": { - "title": "Video Processing", - "author": "humanbydefinition", - "featured": false - }, - "threejs": { - "title": "Three.js Integration", - "author": "humanbydefinition", - "featured": false - }, - "shader-noise": { - "title": "Shader Noise", - "author": "humanbydefinition", - "featured": false - }, - "primitives": { - "title": "Primitives Showcase", - "author": "humanbydefinition", - "featured": false - }, - "p5js": { - "title": "p5.js Integration", - "author": "humanbydefinition", - "featured": false - }, - "hydra-synth": { - "title": "Hydra Synth Integration", - "author": "humanbydefinition", - "featured": false - }, - "entity-wave-prop": { - "title": "Entity Based Wave Propagation", - "author": "trintlermint", - "featured": true - } -} diff --git a/.vitepress/theme/components/ExampleSketchBrowser/ExampleSketchBrowser.vue b/.vitepress/theme/components/ExampleSketchBrowser/ExampleSketchBrowser.vue index efc55ae4..0a762e6b 100644 --- a/.vitepress/theme/components/ExampleSketchBrowser/ExampleSketchBrowser.vue +++ b/.vitepress/theme/components/ExampleSketchBrowser/ExampleSketchBrowser.vue @@ -1,246 +1,143 @@ diff --git a/.vitepress/theme/components/FeaturedSketches/FeaturedSketches.vue b/.vitepress/theme/components/FeaturedSketches/FeaturedSketches.vue index 4120073b..e76cdc93 100644 --- a/.vitepress/theme/components/FeaturedSketches/FeaturedSketches.vue +++ b/.vitepress/theme/components/FeaturedSketches/FeaturedSketches.vue @@ -5,7 +5,7 @@