From 61686d1a2d56ba7e33910d54408fdaf8040bb268 Mon Sep 17 00:00:00 2001 From: Pranav Agale Date: Mon, 22 Jun 2026 11:18:11 +0000 Subject: [PATCH 1/3] feat(examples): add flashcard-app example with flip interaction and keyboard navigation --- examples/flashcard-app/package.json | 24 +++ examples/flashcard-app/src/index.tsx | 277 +++++++++++++++++++++++++++ examples/flashcard-app/tsconfig.json | 10 + 3 files changed, 311 insertions(+) create mode 100644 examples/flashcard-app/package.json create mode 100644 examples/flashcard-app/src/index.tsx create mode 100644 examples/flashcard-app/tsconfig.json diff --git a/examples/flashcard-app/package.json b/examples/flashcard-app/package.json new file mode 100644 index 00000000..4173b6d4 --- /dev/null +++ b/examples/flashcard-app/package.json @@ -0,0 +1,24 @@ +{ + "name": "@termuijs/example-flashcard-app", + "version": "0.1.0", + "private": true, + "description": "Flashcard study app — built with @termuijs/widgets", + "type": "module", + "scripts": { + "start": "bun src/index.tsx", + "dev": "bun --watch src/index.tsx", + "build": "echo 'no build needed'", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@termuijs/core": "workspace:*", + "@termuijs/widgets": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.3", + "@types/node": "^22.0.0" + }, + "engines": { + "bun": ">=1.3.0" + } +} diff --git a/examples/flashcard-app/src/index.tsx b/examples/flashcard-app/src/index.tsx new file mode 100644 index 00000000..16a0a18d --- /dev/null +++ b/examples/flashcard-app/src/index.tsx @@ -0,0 +1,277 @@ +// ───────────────────────────────────────────────────── +// Flashcard App — built with @termuijs/core + @termuijs/widgets +// +// Showcases: flip interaction, keyboard navigation, +// self-marking (known/unknown), summary screen +// ───────────────────────────────────────────────────── + +import { App, type KeyEvent, type Screen } from '@termuijs/core'; +import { Widget, Box, Text, Center } from '@termuijs/widgets'; + +// ── Types ───────────────────────────────────────────── + +interface Flashcard { + question: string; + answer: string; +} + +// ── Flashcard Data ──────────────────────────────────── + +const CARDS: Flashcard[] = [ + { question: 'What does HTML stand for?', answer: 'HyperText Markup Language' }, + { question: 'What is a closure in JavaScript?', answer: 'A function that retains access to its outer scope even after the outer function returns.' }, + { question: 'What does CSS stand for?', answer: 'Cascading Style Sheets' }, + { question: 'What is the time complexity of binary search?', answer: 'O(log n)' }, + { question: 'What does DOM stand for?', answer: 'Document Object Model' }, + { question: 'What is a REST API?', answer: 'An API that follows Representational State Transfer principles using HTTP methods.' }, + { question: 'What is TypeScript?', answer: 'A strongly typed superset of JavaScript that compiles to plain JavaScript.' }, +]; + +// ── Constants ───────────────────────────────────────── + +const WIDGET_WIDTH = 72; +const WIDGET_HEIGHT = 20; + +// ── FlashcardApp Widget ─────────────────────────────── + +class FlashcardApp extends Widget { + private cardIndex = 0; + private flipped = false; + private known = new Set(); + private unknown = new Set(); + private done = false; + + private _header: Text; + private _cardBody: Text; + private _hint: Text; + private _feedback: Text; + private _footer: Text; + + constructor() { + super({ + flexDirection: 'column', + border: 'double', + borderColor: { type: 'named', name: 'cyan' }, + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + width: WIDGET_WIDTH, + height: WIDGET_HEIGHT, + }); + + this._header = new Text( + this.headerText(), + { bold: true, height: 1, fg: { type: 'named', name: 'cyan' } }, + { align: 'center' } + ); + + const divider = new Text( + '─'.repeat(66), + { height: 1, fg: { type: 'named', name: 'brightBlack' } }, + { align: 'left' } + ); + + const gap1 = new Box({ height: 1 }); + + this._cardBody = new Text( + this.cardBodyText(), + { bold: true, height: 3, fg: { type: 'named', name: 'white' } }, + { align: 'center', wrap: true } + ); + + const gap2 = new Box({ height: 1 }); + + this._hint = new Text( + this.hintText(), + { height: 1, fg: { type: 'named', name: 'brightBlack' } }, + { align: 'center' } + ); + + const gap3 = new Box({ height: 1 }); + + this._feedback = new Text( + '', + { bold: true, height: 1, fg: { type: 'named', name: 'green' } }, + { align: 'center' } + ); + + const gap4 = new Box({ height: 1 }); + + this._footer = new Text( + this.footerText(), + { height: 1, fg: { type: 'named', name: 'brightBlack' } }, + { align: 'center' } + ); + + this.addChild(this._header); + this.addChild(divider); + this.addChild(gap1); + this.addChild(this._cardBody); + this.addChild(gap2); + this.addChild(this._hint); + this.addChild(gap3); + this.addChild(this._feedback); + this.addChild(gap4); + this.addChild(this._footer); + } + + // ── Text helpers ────────────────────────────────── + + private currentCard(): Flashcard { + return CARDS[this.cardIndex]; + } + + private headerText(): string { + if (this.done) return ' Study Complete! '; + const side = this.flipped ? 'Answer' : 'Question'; + return ` Card ${this.cardIndex + 1} / ${CARDS.length} · ${side} `; + } + + private cardBodyText(): string { + if (this.done) { + const pct = Math.round((this.known.size / CARDS.length) * 100); + return `Known: ${this.known.size} Unknown: ${this.unknown.size} Score: ${pct}%`; + } + return this.flipped + ? this.currentCard().answer + : this.currentCard().question; + } + + private hintText(): string { + if (this.done) return ''; + return this.flipped + ? '[ y ] I knew it [ n ] Still learning' + : '[ Space ] Flip to see answer'; + } + + private footerText(): string { + if (this.done) return '[ r ] Restart [ q ] Quit'; + return '[ ← ] Prev [ → ] Next [ q ] Quit'; + } + + // ── State updates ───────────────────────────────── + + private flip(): void { + this.flipped = !this.flipped; + this.refresh(); + } + + private markKnown(): void { + if (!this.flipped) return; + this.known.add(this.cardIndex); + this.unknown.delete(this.cardIndex); + this._feedback.setStyle({ fg: { type: 'named', name: 'green' }, bold: true, height: 1 }); + this._feedback.setContent('Marked as known!'); + this.advance(); + } + + private markUnknown(): void { + if (!this.flipped) return; + this.unknown.add(this.cardIndex); + this.known.delete(this.cardIndex); + this._feedback.setStyle({ fg: { type: 'named', name: 'red' }, bold: true, height: 1 }); + this._feedback.setContent('Keep practicing!'); + this.advance(); + } + + private advance(): void { + if (this.cardIndex < CARDS.length - 1) { + this.cardIndex++; + this.flipped = false; + this.refresh(); + } else { + this.showSummary(); + } + } + + private goNext(): void { + if (this.cardIndex < CARDS.length - 1) { + this.cardIndex++; + this.flipped = false; + this._feedback.setContent(''); + this.refresh(); + } + } + + private goPrev(): void { + if (this.cardIndex > 0) { + this.cardIndex--; + this.flipped = false; + this._feedback.setContent(''); + this.refresh(); + } + } + + private showSummary(): void { + this.done = true; + this.refresh(); + } + + private restart(): void { + this.cardIndex = 0; + this.flipped = false; + this.done = false; + this.known.clear(); + this.unknown.clear(); + this._feedback.setContent(''); + this.refresh(); + } + + private refresh(): void { + this._header.setContent(this.headerText()); + this._cardBody.setContent(this.cardBodyText()); + this._hint.setContent(this.hintText()); + this._footer.setContent(this.footerText()); + this.markDirty(); + } + + // ── Key handling ────────────────────────────────── + + handleKey(event: KeyEvent): boolean { + if (event.key === 'q' || (event.ctrl && event.key === 'c')) return false; + + if (this.done) { + if (event.key === 'r') this.restart(); + return true; + } + + if (event.key === 'space') { this.flip(); return true; } + if (event.key === 'y') { this.markKnown(); return true; } + if (event.key === 'n') { this.markUnknown(); return true; } + if (event.key === 'right') { this.goNext(); return true; } + if (event.key === 'left') { this.goPrev(); return true; } + + return true; + } + + protected _renderSelf(_screen: Screen): void { + // children handle all rendering + } +} + +// ── Application entry ───────────────────────────────── + +async function main() { + const flashcard = new FlashcardApp(); + + const center = new Center({}, { horizontal: true, vertical: true }); + center.addChild(flashcard); + + const app = new App(center, { + fullscreen: true, + title: 'TermUI Flashcard App', + fps: 30, + }); + + app.events.on('key', (event: KeyEvent) => { + const shouldContinue = flashcard.handleKey(event); + if (!shouldContinue) app.exit(0); + app.requestRender(); + }); + + const exitCode = await app.mount(); + process.exit(exitCode); +} + +main().catch((err) => { + console.error('Flashcard app error:', err); + process.exit(1); +}); diff --git a/examples/flashcard-app/tsconfig.json b/examples/flashcard-app/tsconfig.json new file mode 100644 index 00000000..dfbefff8 --- /dev/null +++ b/examples/flashcard-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 7558b1b7db66600d5bbf5d79381760595b6a9a90 Mon Sep 17 00:00:00 2001 From: Pranav Agale Date: Mon, 22 Jun 2026 11:18:32 +0000 Subject: [PATCH 2/3] feat: add flashcard-app example configuration in bun.lock --- bun.lock | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/bun.lock b/bun.lock index 50a77b37..52536057 100644 --- a/bun.lock +++ b/bun.lock @@ -139,6 +139,18 @@ "typescript": "^5.4.0", }, }, + "examples/flashcard-app": { + "name": "@termuijs/example-flashcard-app", + "version": "0.1.0", + "dependencies": { + "@termuijs/core": "workspace:*", + "@termuijs/widgets": "workspace:*", + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.9.3", + }, + }, "examples/forms-and-validation": { "name": "@termuijs/example-forms", "version": "0.1.0", @@ -806,6 +818,8 @@ "@termuijs/example-file-manager": ["@termuijs/example-file-manager@workspace:examples/file-manager"], + "@termuijs/example-flashcard-app": ["@termuijs/example-flashcard-app@workspace:examples/flashcard-app"], + "@termuijs/example-forms": ["@termuijs/example-forms@workspace:examples/forms-and-validation"], "@termuijs/example-git-client": ["@termuijs/example-git-client@workspace:examples/git-client"], @@ -1170,6 +1184,8 @@ "@termuijs/data/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], + "@termuijs/example-flashcard-app/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], + "@termuijs/example-quiz-app/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], "@termuijs/example-todo/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], @@ -1200,6 +1216,8 @@ "@termuijs/data/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@termuijs/example-flashcard-app/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@termuijs/example-quiz-app/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@termuijs/example-todo/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], From 9c26dcb583b99eeaaeb3775aae17de4fc146c2e8 Mon Sep 17 00:00:00 2001 From: Pranav Agale Date: Mon, 22 Jun 2026 11:36:32 +0000 Subject: [PATCH 3/3] fix: clear feedback content when showing summary in FlashcardApp --- examples/flashcard-app/src/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/flashcard-app/src/index.tsx b/examples/flashcard-app/src/index.tsx index 16a0a18d..35016015 100644 --- a/examples/flashcard-app/src/index.tsx +++ b/examples/flashcard-app/src/index.tsx @@ -201,9 +201,10 @@ class FlashcardApp extends Widget { } private showSummary(): void { - this.done = true; - this.refresh(); - } + this.done = true; + this._feedback.setContent(''); + this.refresh(); +} private restart(): void { this.cardIndex = 0;