Compare commits

...

11 Commits

Author SHA1 Message Date
semantic-release-bot
7675e59868 chore(release): 4.6.9 [skip ci]
## [4.6.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.8...v4.6.9) (2025-10-18)

### Bug Fixes

* **complement-race:** add missing AI commentary cooldown updates ([357aa30](357aa30618))

### Code Refactoring

* remove dead Python bridge and unused packages ([22426f6](22426f677f))
2025-10-18 14:15:20 +00:00
Thomas Hallock
357aa30618 fix(complement-race): add missing AI commentary cooldown updates
Fixed critical bug where AI racers would spam speech bubbles every 200ms
instead of respecting the 2-6 second cooldown between comments.

Issue:
The TRIGGER_AI_COMMENTARY action was only updating activeSpeechBubbles
but not updating the AI racer's lastComment timestamp and cooldown value.
This caused getAICommentary() cooldown check to always pass since
lastComment stayed at 0.

Fix:
Added setClientAIRacers() call to update racer.lastComment and
racer.commentCooldown (random 2-6 seconds) when commentary is triggered,
matching the pattern from the original single-player version.

Impact:
- AI commentary now properly throttled (one comment per 2-6 seconds)
- Speech bubbles readable instead of rapidly changing
- Matches original solo game behavior

Provider.tsx:803-814

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 09:14:05 -05:00
Thomas Hallock
22426f677f refactor: remove dead Python bridge and unused packages
Removed abandoned SVG generation code that was never used in production:

**Deleted Files:**
- packages/core/src/bridge.py (302 lines) - Python-shell bridge for SVG generation
- packages/core/client/node/src/soroban-generator-bridge.ts - TypeScript wrapper
- packages/core/client/typescript/ - Entire unused @soroban/client package
- packages/core/client/browser/ - Empty package

**Dependencies Removed:**
- python-shell - Only used by abandoned bridge code
- @types/minimatch - Only needed by removed TypeScript packages
- @soroban/client from apps/web

**Code Cleanup:**
- Simplified packages/core/client/node/src/index.ts exports
- Removed SorobanGeneratorBridge, BridgeFlashcardConfig, BridgeFlashcardResult exports

**Impact:**
- ~800 lines of dead TypeScript code removed
- 302 lines of unused Python code removed
- 2 npm dependencies removed
- Build verified successful - no functionality affected

**What Remains Active:**
- generate.py - PDF generation via Typst CLI (actively used by /api/generate)
- soroban-generator.ts - CLI wrapper for PDF generation
- api.py - Optional FastAPI server
- generate_examples.py - Documentation image generator
- Web app uses @soroban/abacus-react for all SVG rendering

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 09:11:59 -05:00
semantic-release-bot
cf997b9cbc chore(release): 4.6.8 [skip ci]
## [4.6.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.7...v4.6.8) (2025-10-18)

### Bug Fixes

* **complement-race:** counter-flip AI speech bubbles to make text readable ([07d5607](07d5607218))
2025-10-18 14:07:19 +00:00
Thomas Hallock
07d5607218 fix(complement-race): counter-flip AI speech bubbles to make text readable
When AI racers were flipped with scaleX(-1) to face right, their speech
bubbles were also flipped, making the text appear mirrored/backwards.

Added a wrapper div around SpeechBubble with scaleX(-1) to counter the
parent's flip transform, keeping the text readable while the emoji
remains flipped.

This matches the pattern used in CircularTrack which counter-rotates
speech bubbles to keep them upright.

LinearTrack.tsx:145-154

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 09:06:25 -05:00
semantic-release-bot
614a081ca6 chore(release): 4.6.7 [skip ci]
## [4.6.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.6...v4.6.7) (2025-10-18)

### Bug Fixes

* **complement-race:** use active local players pattern from navbar ([71cdc34](71cdc342c9))
2025-10-18 14:03:39 +00:00
Thomas Hallock
71cdc342c9 fix(complement-race): use active local players pattern from navbar
Fixed player emoji selection to be consistent with navbar's player
display logic. Both LinearTrack and CircularTrack now use the same
pattern as PageWithNav to get the current user's active local players.

Pattern:
- Get activePlayers Set from GameModeContext
- Map to player objects
- Filter for isLocal !== false (excludes remote players)
- Take first player's emoji

This ensures:
- Consistency with navbar display
- Correct handling of solo vs arcade room modes
- Proper filtering of remote players (isLocal === false)
- Future-proof for multi-player support

Files changed:
- LinearTrack.tsx:23,27-30
- CircularTrack.tsx:20,26-29

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 09:02:45 -05:00
semantic-release-bot
214b9077ab chore(release): 4.6.6 [skip ci]
## [4.6.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.5...v4.6.6) (2025-10-18)

### Bug Fixes

* **complement-race:** use local player emoji instead of first active player ([76eb051](76eb0517c2))
2025-10-18 13:58:33 +00:00
Thomas Hallock
76eb0517c2 fix(complement-race): use local player emoji instead of first active player
Fixed a bug where both LinearTrack and CircularTrack were displaying
the first active player's emoji instead of the current user's (local)
player emoji. This would cause incorrect avatar display in multi-player
arcade rooms.

Changed from:
- Getting first element of activePlayers array
- Would show wrong player in multi-player scenarios

To:
- Finding player with isLocal === true
- Correctly shows current user's avatar
- Matches pattern used throughout Provider.tsx

Files changed:
- LinearTrack.tsx: Use local player for emoji
- CircularTrack.tsx: Use local player for emoji

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:57:45 -05:00
semantic-release-bot
820000f93b chore(release): 4.6.5 [skip ci]
## [4.6.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.4...v4.6.5) (2025-10-18)

### Bug Fixes

* **complement-race:** flip player avatar to face right in practice mode ([fa6b3b6](fa6b3b69d5))
2025-10-18 13:48:07 +00:00
Thomas Hallock
fa6b3b69d5 fix(complement-race): flip player avatar to face right in practice mode
Adds scaleX(-1) to player racer icon so it faces the same direction as AI racers (racing to the right).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:47:11 -05:00
39 changed files with 92 additions and 1566 deletions

View File

@@ -1,3 +1,43 @@
## [4.6.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.8...v4.6.9) (2025-10-18)
### Bug Fixes
* **complement-race:** add missing AI commentary cooldown updates ([357aa30](https://github.com/antialias/soroban-abacus-flashcards/commit/357aa30618f80d659ae515f94b7b9254bb458910))
### Code Refactoring
* remove dead Python bridge and unused packages ([22426f6](https://github.com/antialias/soroban-abacus-flashcards/commit/22426f677f9b127441377b95571f0066a0990d3f))
## [4.6.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.7...v4.6.8) (2025-10-18)
### Bug Fixes
* **complement-race:** counter-flip AI speech bubbles to make text readable ([07d5607](https://github.com/antialias/soroban-abacus-flashcards/commit/07d5607218aee03e813eceff5d161a7838d66bcb))
## [4.6.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.6...v4.6.7) (2025-10-18)
### Bug Fixes
* **complement-race:** use active local players pattern from navbar ([71cdc34](https://github.com/antialias/soroban-abacus-flashcards/commit/71cdc342c97ca53b5e7e4202d4d344199e8ddd98))
## [4.6.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.5...v4.6.6) (2025-10-18)
### Bug Fixes
* **complement-race:** use local player emoji instead of first active player ([76eb051](https://github.com/antialias/soroban-abacus-flashcards/commit/76eb0517c202d1b9160b49dec0b99ff4972daff2))
## [4.6.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.4...v4.6.5) (2025-10-18)
### Bug Fixes
* **complement-race:** flip player avatar to face right in practice mode ([fa6b3b6](https://github.com/antialias/soroban-abacus-flashcards/commit/fa6b3b69d5a4a7eb70f8c18fc8c122c54c4d504a))
## [4.6.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.3...v4.6.4) (2025-10-18)

View File

@@ -92,7 +92,9 @@
"Bash(ls:*)",
"Bash(do if [ -f \"$file\" ])",
"Bash(! echo \"$file\")",
"Bash(then sed -i '' \"s|from ''''../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" sed -i '' \"s|from ''''../../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" fi done)"
"Bash(then sed -i '' \"s|from ''''../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" sed -i '' \"s|from ''''../../context/ComplementRaceContext''''|from ''''@/arcade-games/complement-race/Provider''''|g\" \"$file\" fi done)",
"Bash(pnpm install)",
"Bash(pnpm exec turbo build --filter=@soroban/web)"
],
"deny": [],
"ask": []

View File

@@ -48,7 +48,6 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@react-spring/web": "^10.0.2",
"@soroban/abacus-react": "workspace:*",
"@soroban/client": "workspace:*",
"@soroban/core": "workspace:*",
"@soroban/templates": "workspace:*",
"@tanstack/react-form": "^0.19.0",

View File

@@ -17,15 +17,16 @@ interface CircularTrackProps {
export function CircularTrack({ playerProgress, playerLap, aiRacers, aiLaps }: CircularTrackProps) {
const { state, dispatch } = useComplementRace()
const { players } = useGameMode()
const { players, activePlayers } = useGameMode()
const { profile: _profile } = useUserProfile()
const { playSound } = useSoundEffects()
const [celebrationCooldown, setCelebrationCooldown] = useState<Set<string>>(new Set())
// Get the first active player's emoji
const activePlayers = Array.from(players.values()).filter((p) => p.id)
const firstActivePlayer = activePlayers[0]
const playerEmoji = firstActivePlayer?.emoji ?? '👤'
// Get the current user's active local players (consistent with navbar pattern)
const activeLocalPlayers = Array.from(activePlayers)
.map((id) => players.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined && p.isLocal !== false)
const playerEmoji = activeLocalPlayers[0]?.emoji ?? '👤'
const [dimensions, setDimensions] = useState({ width: 600, height: 400 })
// Update dimensions on mount and resize

View File

@@ -20,13 +20,14 @@ export function LinearTrack({
showFinishLine = true,
}: LinearTrackProps) {
const { state, dispatch } = useComplementRace()
const { players } = useGameMode()
const { players, activePlayers } = useGameMode()
const { profile: _profile } = useUserProfile()
// Get the first active player's emoji
const activePlayers = Array.from(players.values()).filter((p) => p.id)
const firstActivePlayer = activePlayers[0]
const playerEmoji = firstActivePlayer?.emoji ?? '👤'
// Get the current user's active local players (consistent with navbar pattern)
const activeLocalPlayers = Array.from(activePlayers)
.map((id) => players.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined && p.isLocal !== false)
const playerEmoji = activeLocalPlayers[0]?.emoji ?? '👤'
// Position calculation: leftPercent = Math.min(98, (progress / raceGoal) * 96 + 2)
// 2% minimum (start), 98% maximum (near finish), 96% range for race
@@ -110,7 +111,7 @@ export function LinearTrack({
position: 'absolute',
left: `${playerPosition}%`,
top: '50%',
transform: 'translate(-50%, -50%)',
transform: 'translate(-50%, -50%) scaleX(-1)',
fontSize: '32px',
transition: 'left 0.3s ease-out',
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))',
@@ -141,10 +142,16 @@ export function LinearTrack({
>
{racer.icon}
{activeBubble && (
<SpeechBubble
message={activeBubble}
onHide={() => dispatch({ type: 'CLEAR_AI_COMMENT', racerId: racer.id })}
/>
<div
style={{
transform: 'scaleX(-1)', // Counter-flip bubble to make text readable
}}
>
<SpeechBubble
message={activeBubble}
onHide={() => dispatch({ type: 'CLEAR_AI_COMMENT', racerId: racer.id })}
/>
</div>
)}
</div>
)

View File

@@ -800,6 +800,18 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
newBubbles.set(action.racerId, action.message)
return { ...prev, activeSpeechBubbles: newBubbles }
})
// Update racer's lastComment time and cooldown to prevent spam
setClientAIRacers((prevRacers) =>
prevRacers.map((racer) =>
racer.id === action.racerId
? {
...racer,
lastComment: Date.now(),
commentCooldown: Math.random() * 4000 + 2000, // 2-6 seconds
}
: racer
)
)
break
}
case 'CLEAR_AI_COMMENT': {

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "4.6.4",
"version": "4.6.9",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi

View File

@@ -1 +0,0 @@
../../../../../../node_modules/.pnpm/@myriaddreamin+typst-ts-renderer@0.6.0/node_modules/@myriaddreamin/typst-ts-renderer

View File

@@ -1 +0,0 @@
../../../../../../node_modules/.pnpm/@myriaddreamin+typst-ts-web-compiler@0.6.0/node_modules/@myriaddreamin/typst-ts-web-compiler

View File

@@ -1 +0,0 @@
../../../../../node_modules/.pnpm/vite@5.4.20_@types+node@20.19.19_terser@5.44.0/node_modules/vite

View File

@@ -1,19 +0,0 @@
{
"name": "soroban-flashcards-browser",
"version": "1.0.0",
"description": "Browser-based Soroban Flashcard Generator using Typst.ts",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@myriaddreamin/typst-ts-web-compiler": "^0.6.0",
"@myriaddreamin/typst-ts-renderer": "^0.6.0"
},
"devDependencies": {
"vite": "^5.0.0",
"typescript": "^5.0.0"
}
}

View File

@@ -2,12 +2,12 @@
* Node.js TypeScript wrapper for Soroban Flashcard Generator
* Calls Python functions via child_process
*/
interface FlashcardConfig$1 {
interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
orientation?: 'portrait' | 'landscape';
paperSize?: "us-letter" | "a4" | "a3" | "a5";
orientation?: "portrait" | "landscape";
margins?: {
top?: string;
bottom?: string;
@@ -24,12 +24,12 @@ interface FlashcardConfig$1 {
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: 'diamond' | 'circle' | 'square';
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
beadShape?: "diamond" | "circle" | "square";
colorScheme?: "monochrome" | "place-value" | "heaven-earth" | "alternating";
coloredNumerals?: boolean;
scaleFactor?: number;
}
declare class SorobanGenerator$1 {
declare class SorobanGenerator {
private pythonPath;
private generatorPath;
private projectRoot;
@@ -38,15 +38,15 @@ declare class SorobanGenerator$1 {
/**
* Generate flashcards and return PDF as Buffer
*/
generate(config: FlashcardConfig$1): Promise<Buffer>;
generate(config: FlashcardConfig): Promise<Buffer>;
/**
* Generate flashcards and save to file
*/
generateToFile(config: FlashcardConfig$1, outputPath: string): Promise<void>;
generateToFile(config: FlashcardConfig, outputPath: string): Promise<void>;
/**
* Generate flashcards and return as base64 string
*/
generateBase64(config: FlashcardConfig$1): Promise<string>;
generateBase64(config: FlashcardConfig): Promise<string>;
private executePython;
/**
* Check if all dependencies are installed
@@ -59,65 +59,4 @@ declare class SorobanGenerator$1 {
}
declare function expressExample(): Promise<void>;
/**
* TypeScript wrapper using python-shell for clean function interface
* No CLI arguments - just function calls with objects
*/
interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
orientation?: 'portrait' | 'landscape';
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: 'diamond' | 'circle' | 'square';
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
coloredNumerals?: boolean;
scaleFactor?: number;
format?: 'pdf' | 'svg';
mode?: 'single-card' | 'flashcards';
number?: number;
}
interface FlashcardResult {
pdf: string;
count: number;
numbers: number[];
}
declare class SorobanGenerator {
private pythonShell;
private projectRoot;
constructor(projectRoot?: string);
/**
* Initialize persistent Python process for better performance
*/
initialize(): Promise<void>;
/**
* Generate flashcards - clean function interface
*/
generate(config: FlashcardConfig): Promise<FlashcardResult>;
/**
* Generate and return as Buffer
*/
generateBuffer(config: FlashcardConfig): Promise<Buffer>;
/**
* Clean up Python process
*/
close(): Promise<void>;
}
export { FlashcardConfig as BridgeFlashcardConfig, FlashcardResult as BridgeFlashcardResult, FlashcardConfig$1 as FlashcardConfig, SorobanGenerator$1 as SorobanGenerator, SorobanGenerator as SorobanGeneratorBridge, SorobanGenerator$1 as default, expressExample };
export { type FlashcardConfig, SorobanGenerator, SorobanGenerator as default, expressExample };

View File

@@ -31,7 +31,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var src_exports = {};
__export(src_exports, {
SorobanGenerator: () => SorobanGenerator,
SorobanGeneratorBridge: () => SorobanGenerator2,
default: () => SorobanGenerator,
expressExample: () => expressExample
});
@@ -194,96 +193,8 @@ var SorobanGenerator = class {
async function expressExample() {
const generator = new SorobanGenerator();
}
// src/soroban-generator-bridge.ts
var import_python_shell = require("python-shell");
var path2 = __toESM(require("path"));
var SorobanGenerator2 = class {
pythonShell = null;
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot || path2.join(__dirname, "../../");
}
/**
* Initialize persistent Python process for better performance
*/
async initialize() {
if (this.pythonShell)
return;
this.pythonShell = new import_python_shell.PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
pythonOptions: ["-u"],
// Unbuffered
scriptPath: this.projectRoot
});
}
/**
* Generate flashcards - clean function interface
*/
async generate(config) {
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
const shell = new import_python_shell.PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot
});
shell.on("message", (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
});
shell.on("error", (err) => {
reject(err);
});
shell.send(config);
shell.end((err, code, signal) => {
if (err)
reject(err);
});
});
}
return new Promise((resolve, reject) => {
if (!this.pythonShell) {
reject(new Error("Not initialized"));
return;
}
const handler = (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
this.pythonShell?.removeListener("message", handler);
};
this.pythonShell.on("message", handler);
this.pythonShell.send(config);
});
}
/**
* Generate and return as Buffer
*/
async generateBuffer(config) {
const result = await this.generate(config);
return Buffer.from(result.pdf, "base64");
}
/**
* Clean up Python process
*/
async close() {
if (this.pythonShell) {
this.pythonShell.end(() => {
});
this.pythonShell = null;
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SorobanGenerator,
SorobanGeneratorBridge,
expressExample
});

View File

@@ -155,96 +155,8 @@ var SorobanGenerator = class {
async function expressExample() {
const generator = new SorobanGenerator();
}
// src/soroban-generator-bridge.ts
import { PythonShell } from "python-shell";
import * as path2 from "path";
var SorobanGenerator2 = class {
pythonShell = null;
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot || path2.join(__dirname, "../../");
}
/**
* Initialize persistent Python process for better performance
*/
async initialize() {
if (this.pythonShell)
return;
this.pythonShell = new PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
pythonOptions: ["-u"],
// Unbuffered
scriptPath: this.projectRoot
});
}
/**
* Generate flashcards - clean function interface
*/
async generate(config) {
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
const shell = new PythonShell(path2.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot
});
shell.on("message", (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
});
shell.on("error", (err) => {
reject(err);
});
shell.send(config);
shell.end((err, code, signal) => {
if (err)
reject(err);
});
});
}
return new Promise((resolve, reject) => {
if (!this.pythonShell) {
reject(new Error("Not initialized"));
return;
}
const handler = (message) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message);
}
this.pythonShell?.removeListener("message", handler);
};
this.pythonShell.on("message", handler);
this.pythonShell.send(config);
});
}
/**
* Generate and return as Buffer
*/
async generateBuffer(config) {
const result = await this.generate(config);
return Buffer.from(result.pdf, "base64");
}
/**
* Clean up Python process
*/
async close() {
if (this.pythonShell) {
this.pythonShell.end(() => {
});
this.pythonShell = null;
}
}
};
export {
SorobanGenerator,
SorobanGenerator2 as SorobanGeneratorBridge,
SorobanGenerator as default,
expressExample
};

View File

@@ -6,9 +6,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.19.12/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
"$basedir/../../../../../../node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/esbuild" "$@"
"$basedir/../../../../../../node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild/bin/esbuild" "$@"
exit $?

View File

@@ -1 +0,0 @@
../../../../../node_modules/.pnpm/python-shell@5.0.0/node_modules/python-shell

View File

@@ -18,11 +18,7 @@
"type-check": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"python-shell": "^5.0.0"
},
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^20.0.0",
"tsup": "^7.0.0",
"typescript": "^5.0.0",

View File

@@ -5,12 +5,5 @@
export * from "./soroban-generator";
// Export bridge generator with different name to avoid conflicts
export {
SorobanGenerator as SorobanGeneratorBridge,
FlashcardConfig as BridgeFlashcardConfig,
FlashcardResult as BridgeFlashcardResult,
} from "./soroban-generator-bridge";
// Default export for convenience
export { SorobanGenerator as default } from "./soroban-generator";

View File

@@ -1,190 +0,0 @@
/**
* TypeScript wrapper using python-shell for clean function interface
* No CLI arguments - just function calls with objects
*/
import { PythonShell } from "python-shell";
import * as path from "path";
export interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: "us-letter" | "a4" | "a3" | "a5";
orientation?: "portrait" | "landscape";
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: "diamond" | "circle" | "square";
colorScheme?: "monochrome" | "place-value" | "heaven-earth" | "alternating";
coloredNumerals?: boolean;
scaleFactor?: number;
format?: "pdf" | "svg";
mode?: "single-card" | "flashcards";
number?: number;
}
export interface FlashcardResult {
pdf: string; // base64 encoded PDF or SVG content (depending on format)
count: number;
numbers: number[];
}
export class SorobanGenerator {
private pythonShell: PythonShell | null = null;
private projectRoot: string;
constructor(projectRoot?: string) {
this.projectRoot = projectRoot || path.join(__dirname, "../../");
}
/**
* Initialize persistent Python process for better performance
*/
async initialize(): Promise<void> {
if (this.pythonShell) return;
this.pythonShell = new PythonShell(path.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
pythonOptions: ["-u"], // Unbuffered
scriptPath: this.projectRoot,
});
}
/**
* Generate flashcards - clean function interface
*/
async generate(config: FlashcardConfig): Promise<FlashcardResult> {
// One-shot mode if not initialized
if (!this.pythonShell) {
return new Promise((resolve, reject) => {
const shell = new PythonShell(path.join("src", "bridge.py"), {
mode: "json",
pythonPath: "python3",
scriptPath: this.projectRoot,
});
shell.on("message", (message: any) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message as FlashcardResult);
}
});
shell.on("error", (err: any) => {
reject(err);
});
shell.send(config);
shell.end((err: any, code: any, signal: any) => {
if (err) reject(err);
});
});
}
// Persistent mode
return new Promise((resolve, reject) => {
if (!this.pythonShell) {
reject(new Error("Not initialized"));
return;
}
const handler = (message: any) => {
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message as FlashcardResult);
}
this.pythonShell?.removeListener("message", handler);
};
this.pythonShell.on("message", handler);
this.pythonShell.send(config);
});
}
/**
* Generate and return as Buffer
*/
async generateBuffer(config: FlashcardConfig): Promise<Buffer> {
const result = await this.generate(config);
return Buffer.from(result.pdf, "base64");
}
/**
* Clean up Python process
*/
async close(): Promise<void> {
if (this.pythonShell) {
this.pythonShell.end(() => {});
this.pythonShell = null;
}
}
}
// Example usage - just like calling a regular TypeScript function
async function example() {
const generator = new SorobanGenerator();
// Just call it like a function!
const result = await generator.generate({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true,
});
// You get back a clean result object
console.log(`Generated ${result.count} flashcards`);
// Convert to Buffer if needed
const pdfBuffer = Buffer.from(result.pdf, "base64");
// Or use persistent mode for better performance
await generator.initialize();
// Now calls are faster
const result2 = await generator.generate({ range: "0-9" });
const result3 = await generator.generate({ range: "10-19" });
await generator.close();
}
// Express example - clean function calls
export function expressRoute(app: any) {
const generator = new SorobanGenerator();
app.post("/api/flashcards", async (req: any, res: any) => {
try {
// Just pass the config object directly!
const result = await generator.generate(req.body);
// Send back JSON or PDF
if (req.query.format === "json") {
res.json(result);
} else {
const pdfBuffer = Buffer.from(result.pdf, "base64");
res.contentType("application/pdf");
res.send(pdfBuffer);
}
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
}

View File

@@ -1,58 +0,0 @@
/**
* TypeScript client for Soroban Flashcard Generator API
*/
interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
orientation?: 'portrait' | 'landscape';
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: 'diamond' | 'circle' | 'square';
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
coloredNumerals?: boolean;
scaleFactor?: number;
}
interface FlashcardResponse {
pdf: string;
count: number;
numbers: number[];
}
declare class SorobanFlashcardClient {
private apiUrl;
constructor(apiUrl?: string);
/**
* Generate flashcards and return as base64 PDF
*/
generate(config: FlashcardConfig): Promise<FlashcardResponse>;
/**
* Generate flashcards and download as PDF file
*/
generateAndDownload(config: FlashcardConfig, filename?: string): Promise<void>;
/**
* Generate flashcards and open in new tab
*/
generateAndOpen(config: FlashcardConfig): Promise<void>;
/**
* Check API health
*/
health(): Promise<boolean>;
}
declare function example(): Promise<void>;
export { FlashcardConfig, FlashcardResponse, SorobanFlashcardClient, SorobanFlashcardClient as default, example };

View File

@@ -1,148 +0,0 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
SorobanFlashcardClient: () => SorobanFlashcardClient,
default: () => SorobanFlashcardClient,
example: () => example
});
module.exports = __toCommonJS(src_exports);
// src/soroban-flashcards.ts
var SorobanFlashcardClient = class {
apiUrl;
constructor(apiUrl = "http://localhost:8000") {
this.apiUrl = apiUrl;
}
/**
* Generate flashcards and return as base64 PDF
*/
async generate(config) {
const response = await fetch(`${this.apiUrl}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
range: config.range,
step: config.step ?? 1,
cards_per_page: config.cardsPerPage ?? 6,
paper_size: config.paperSize ?? "us-letter",
orientation: config.orientation ?? "portrait",
margins: config.margins ?? {
top: "0.5in",
bottom: "0.5in",
left: "0.5in",
right: "0.5in"
},
gutter: config.gutter ?? "5mm",
shuffle: config.shuffle ?? false,
seed: config.seed,
show_cut_marks: config.showCutMarks ?? false,
show_registration: config.showRegistration ?? false,
font_family: config.fontFamily ?? "DejaVu Sans",
font_size: config.fontSize ?? "48pt",
columns: config.columns ?? "auto",
show_empty_columns: config.showEmptyColumns ?? false,
hide_inactive_beads: config.hideInactiveBeads ?? false,
bead_shape: config.beadShape ?? "diamond",
color_scheme: config.colorScheme ?? "monochrome",
colored_numerals: config.coloredNumerals ?? false,
scale_factor: config.scaleFactor ?? 0.9,
format: "base64"
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to generate flashcards");
}
return response.json();
}
/**
* Generate flashcards and download as PDF file
*/
async generateAndDownload(config, filename = "flashcards.pdf") {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Generate flashcards and open in new tab
*/
async generateAndOpen(config) {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
/**
* Check API health
*/
async health() {
try {
const response = await fetch(`${this.apiUrl}/health`);
const data = await response.json();
return data.status === "healthy";
} catch {
return false;
}
}
};
async function example() {
const client = new SorobanFlashcardClient();
await client.generateAndDownload({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true
});
await client.generateAndDownload(
{
range: "0-100",
step: 5,
cardsPerPage: 6
},
"counting-by-5s.pdf"
);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SorobanFlashcardClient,
example
});

View File

@@ -1,120 +0,0 @@
// src/soroban-flashcards.ts
var SorobanFlashcardClient = class {
apiUrl;
constructor(apiUrl = "http://localhost:8000") {
this.apiUrl = apiUrl;
}
/**
* Generate flashcards and return as base64 PDF
*/
async generate(config) {
const response = await fetch(`${this.apiUrl}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
range: config.range,
step: config.step ?? 1,
cards_per_page: config.cardsPerPage ?? 6,
paper_size: config.paperSize ?? "us-letter",
orientation: config.orientation ?? "portrait",
margins: config.margins ?? {
top: "0.5in",
bottom: "0.5in",
left: "0.5in",
right: "0.5in"
},
gutter: config.gutter ?? "5mm",
shuffle: config.shuffle ?? false,
seed: config.seed,
show_cut_marks: config.showCutMarks ?? false,
show_registration: config.showRegistration ?? false,
font_family: config.fontFamily ?? "DejaVu Sans",
font_size: config.fontSize ?? "48pt",
columns: config.columns ?? "auto",
show_empty_columns: config.showEmptyColumns ?? false,
hide_inactive_beads: config.hideInactiveBeads ?? false,
bead_shape: config.beadShape ?? "diamond",
color_scheme: config.colorScheme ?? "monochrome",
colored_numerals: config.coloredNumerals ?? false,
scale_factor: config.scaleFactor ?? 0.9,
format: "base64"
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to generate flashcards");
}
return response.json();
}
/**
* Generate flashcards and download as PDF file
*/
async generateAndDownload(config, filename = "flashcards.pdf") {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Generate flashcards and open in new tab
*/
async generateAndOpen(config) {
const result = await this.generate(config);
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
/**
* Check API health
*/
async health() {
try {
const response = await fetch(`${this.apiUrl}/health`);
const data = await response.json();
return data.status === "healthy";
} catch {
return false;
}
}
};
async function example() {
const client = new SorobanFlashcardClient();
await client.generateAndDownload({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true
});
await client.generateAndDownload(
{
range: "0-100",
step: 5,
cardsPerPage: 6
},
"counting-by-5s.pdf"
);
}
export {
SorobanFlashcardClient,
SorobanFlashcardClient as default,
example
};

View File

@@ -1,14 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/esbuild@0.18.20/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
"$basedir/../../../../../../node_modules/.pnpm/esbuild@0.18.20/node_modules/esbuild/bin/esbuild" "$@"
exit $?

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tsup/dist/cli-default.js" "$@"
else
exec node "$basedir/../tsup/dist/cli-default.js" "$@"
fi

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tsup/dist/cli-node.js" "$@"
else
exec node "$basedir/../tsup/dist/cli-node.js" "$@"
fi

View File

@@ -1,17 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
else
exec node "$basedir/../vitest/vitest.mjs" "$@"
fi

View File

@@ -1 +0,0 @@
../../../../../node_modules/.pnpm/tsup@7.3.0_postcss@8.5.6_typescript@5.9.3/node_modules/tsup

View File

@@ -1 +0,0 @@
../../../../../node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.19_@vitest+ui@3.2.4_happy-dom@18.0.1_jsdom@27.0.0_postcss@8.5.6__terser@5.44.0/node_modules/vitest

View File

@@ -1,38 +0,0 @@
{
"name": "@soroban/client",
"version": "1.0.0",
"description": "TypeScript client for Soroban Flashcard Generator",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.esm.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"test": "vitest",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist"
},
"keywords": [
"soroban",
"abacus",
"flashcards",
"education",
"math"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^20.0.0",
"tsup": "^7.0.0",
"typescript": "^5.0.0",
"vitest": "^1.0.0"
}
}

View File

@@ -1,7 +0,0 @@
/**
* Soroban Flashcard Generator - TypeScript Client
* Re-export main client functionality
*/
export * from "./soroban-flashcards";
export { SorobanFlashcardClient as default } from "./soroban-flashcards";

View File

@@ -1,176 +0,0 @@
/**
* TypeScript client for Soroban Flashcard Generator API
*/
export interface FlashcardConfig {
range: string;
step?: number;
cardsPerPage?: number;
paperSize?: "us-letter" | "a4" | "a3" | "a5";
orientation?: "portrait" | "landscape";
margins?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
gutter?: string;
shuffle?: boolean;
seed?: number;
showCutMarks?: boolean;
showRegistration?: boolean;
fontFamily?: string;
fontSize?: string;
columns?: string | number;
showEmptyColumns?: boolean;
hideInactiveBeads?: boolean;
beadShape?: "diamond" | "circle" | "square";
colorScheme?: "monochrome" | "place-value" | "heaven-earth" | "alternating";
coloredNumerals?: boolean;
scaleFactor?: number;
}
export interface FlashcardResponse {
pdf: string; // base64 encoded PDF
count: number;
numbers: number[];
}
export class SorobanFlashcardClient {
private apiUrl: string;
constructor(apiUrl: string = "http://localhost:8000") {
this.apiUrl = apiUrl;
}
/**
* Generate flashcards and return as base64 PDF
*/
async generate(config: FlashcardConfig): Promise<FlashcardResponse> {
const response = await fetch(`${this.apiUrl}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
range: config.range,
step: config.step ?? 1,
cards_per_page: config.cardsPerPage ?? 6,
paper_size: config.paperSize ?? "us-letter",
orientation: config.orientation ?? "portrait",
margins: config.margins ?? {
top: "0.5in",
bottom: "0.5in",
left: "0.5in",
right: "0.5in",
},
gutter: config.gutter ?? "5mm",
shuffle: config.shuffle ?? false,
seed: config.seed,
show_cut_marks: config.showCutMarks ?? false,
show_registration: config.showRegistration ?? false,
font_family: config.fontFamily ?? "DejaVu Sans",
font_size: config.fontSize ?? "48pt",
columns: config.columns ?? "auto",
show_empty_columns: config.showEmptyColumns ?? false,
hide_inactive_beads: config.hideInactiveBeads ?? false,
bead_shape: config.beadShape ?? "diamond",
color_scheme: config.colorScheme ?? "monochrome",
colored_numerals: config.coloredNumerals ?? false,
scale_factor: config.scaleFactor ?? 0.9,
format: "base64",
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to generate flashcards");
}
return response.json();
}
/**
* Generate flashcards and download as PDF file
*/
async generateAndDownload(
config: FlashcardConfig,
filename: string = "flashcards.pdf",
): Promise<void> {
const result = await this.generate(config);
// Convert base64 to blob
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Generate flashcards and open in new tab
*/
async generateAndOpen(config: FlashcardConfig): Promise<void> {
const result = await this.generate(config);
// Convert base64 to blob
const pdfBytes = atob(result.pdf);
const byteArray = new Uint8Array(pdfBytes.length);
for (let i = 0; i < pdfBytes.length; i++) {
byteArray[i] = pdfBytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: "application/pdf" });
// Open in new tab
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
}
/**
* Check API health
*/
async health(): Promise<boolean> {
try {
const response = await fetch(`${this.apiUrl}/health`);
const data = await response.json();
return data.status === "healthy";
} catch {
return false;
}
}
}
// Example usage function
export async function example() {
const client = new SorobanFlashcardClient();
// Generate and download flashcards for 0-99 with place-value coloring
await client.generateAndDownload({
range: "0-99",
cardsPerPage: 6,
colorScheme: "place-value",
coloredNumerals: true,
showCutMarks: true,
});
// Generate counting by 5s
await client.generateAndDownload(
{
range: "0-100",
step: 5,
cardsPerPage: 6,
},
"counting-by-5s.pdf",
);
}

View File

@@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -1,303 +0,0 @@
#!/usr/bin/env python3
"""
Python bridge for Node.js integration
Provides a clean function interface instead of CLI
"""
import json
import sys
import base64
import tempfile
import os
import glob
from pathlib import Path
import subprocess
# Import our existing functions
from generate import parse_range, generate_typst_file, generate_single_card_typst
def generate_flashcards_json(config_json):
"""
Generate flashcards from JSON config
Returns base64 encoded PDF
"""
config = json.loads(config_json)
# Parse numbers
numbers = parse_range(
config.get('range', '0-9'),
config.get('step', 1)
)
# Handle shuffle
if config.get('shuffle', False):
import random
if 'seed' in config:
random.seed(config['seed'])
random.shuffle(numbers)
# Build Typst config
typst_config = {
'cards_per_page': config.get('cardsPerPage', 6),
'paper_size': config.get('paperSize', 'us-letter'),
'orientation': config.get('orientation', 'portrait'),
'margins': config.get('margins', {
'top': '0.5in',
'bottom': '0.5in',
'left': '0.5in',
'right': '0.5in'
}),
'gutter': config.get('gutter', '5mm'),
'show_cut_marks': config.get('showCutMarks', False),
'show_registration': config.get('showRegistration', False),
'font_family': config.get('fontFamily', 'DejaVu Sans'),
'font_size': config.get('fontSize', '48pt'),
'columns': config.get('columns', 'auto'),
'show_empty_columns': config.get('showEmptyColumns', False),
'hide_inactive_beads': config.get('hideInactiveBeads', False),
'bead_shape': config.get('beadShape', 'diamond'),
'color_scheme': config.get('colorScheme', 'monochrome'),
'colored_numerals': config.get('coloredNumerals', False),
'scale_factor': config.get('scaleFactor', 0.9),
}
# Generate in core package directory to match main generator behavior
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
# Generate Typst file - same setup as main generate.py
core_package_root = Path(__file__).parent.parent # packages/core directory
# Create temp files in core package root, not temp directory
temp_typst = core_package_root / f'temp_flashcards_{os.getpid()}.typ'
temp_pdf = core_package_root / f'temp_flashcards_{os.getpid()}.pdf'
# Convert Python list to Typst array syntax (same as main generate.py)
if numbers:
numbers_str = '(' + ', '.join(str(n) for n in numbers) + ',)'
else:
numbers_str = '()'
# Create temp Typst with relative imports (works when run from core package root)
typst_content = f'''
#import "templates/flashcards.typ": generate-flashcards
#generate-flashcards(
{numbers_str},
cards-per-page: {typst_config['cards_per_page']},
paper-size: "{typst_config['paper_size']}",
orientation: "{typst_config['orientation']}",
margins: (
top: {typst_config['margins'].get('top', '0.5in')},
bottom: {typst_config['margins'].get('bottom', '0.5in')},
left: {typst_config['margins'].get('left', '0.5in')},
right: {typst_config['margins'].get('right', '0.5in')}
),
gutter: {typst_config['gutter']},
show-cut-marks: {str(typst_config['show_cut_marks']).lower()},
show-registration: {str(typst_config['show_registration']).lower()},
font-family: "{typst_config['font_family']}",
font-size: {typst_config['font_size']},
columns: {typst_config['columns']},
show-empty-columns: {str(typst_config['show_empty_columns']).lower()},
hide-inactive-beads: {str(typst_config['hide_inactive_beads']).lower()},
bead-shape: "{typst_config['bead_shape']}",
color-scheme: "{typst_config['color_scheme']}",
colored-numerals: {str(typst_config['colored_numerals']).lower()},
scale-factor: {typst_config['scale_factor']}
)
'''
with open(temp_typst, 'w') as f:
f.write(typst_content)
# Get format preference
output_format = config.get('format', 'pdf').lower()
temp_svg = None
try:
if output_format == 'svg':
# Generate SVG using Typst with page template for multi-page support
temp_svg = core_package_root / f'temp_flashcards_{os.getpid()}_{{p}}.svg'
result = subprocess.run(
['typst', 'compile', str(temp_typst), str(temp_svg), '--format', 'svg'],
capture_output=True,
text=True,
cwd=str(core_package_root)
)
if result.returncode != 0:
return json.dumps({
'error': f'Typst SVG compilation failed: {result.stderr}'
})
# Read SVG content - find the first generated page
svg_pattern = core_package_root / f'temp_flashcards_{os.getpid()}_*.svg'
import glob
svg_files = glob.glob(str(svg_pattern))
if not svg_files:
return json.dumps({
'error': 'No SVG files were generated'
})
# Read the first SVG file (page 1)
svg_file = Path(svg_files[0])
with open(svg_file, 'r', encoding='utf-8') as f:
svg_content = f.read()
# Clean up all generated SVG files
for svg_path in svg_files:
Path(svg_path).unlink()
result_data = {
'pdf': svg_content, # Keep field name for compatibility
'count': len(numbers),
'numbers': numbers[:100]
}
else:
# Generate PDF (default)
result = subprocess.run(
['typst', 'compile', str(temp_typst), str(temp_pdf)],
capture_output=True,
text=True,
cwd=str(core_package_root)
)
if result.returncode != 0:
return json.dumps({
'error': f'Typst compilation failed: {result.stderr}'
})
# Read and encode PDF
with open(temp_pdf, 'rb') as f:
pdf_bytes = f.read()
result_data = {
'pdf': base64.b64encode(pdf_bytes).decode('utf-8'),
'count': len(numbers),
'numbers': numbers[:100] # Limit preview
}
finally:
# Clean up temp files
for temp_file in [temp_typst, temp_pdf, temp_svg if output_format == 'svg' else None]:
if temp_file and temp_file.exists():
temp_file.unlink()
return json.dumps(result_data)
def generate_single_card_json(config_json):
"""
Generate a single card SVG from JSON config
Specifically for preview - always returns front side (abacus)
"""
config = json.loads(config_json)
# Extract the single number
number = config.get('number')
if number is None:
return json.dumps({'error': 'Missing number parameter'})
# Build Typst config optimized for preview display
typst_config = {
'bead_shape': config.get('beadShape', 'diamond'),
'color_scheme': config.get('colorScheme', 'monochrome'),
'color_palette': config.get('colorPalette', 'default'),
'colored_numerals': config.get('coloredNumerals', False),
'hide_inactive_beads': config.get('hideInactiveBeads', False),
'show_empty_columns': config.get('showEmptyColumns', False),
'columns': config.get('columns', 'auto'),
'transparent': config.get('transparent', False),
'card_width': '120pt', # Smaller card for larger abacus
'card_height': '160pt', # Smaller card for larger abacus
'font_size': config.get('fontSize', '48pt'),
'font_family': config.get('fontFamily', 'DejaVu Sans'),
'scale_factor': config.get('scaleFactor', 4.0), # Much larger scale for preview visibility
}
# Generate in core package directory
core_package_root = Path(__file__).parent.parent
temp_typst = core_package_root / f'temp_single_{number}_{os.getpid()}.typ'
temp_svg = core_package_root / f'temp_single_{number}_{os.getpid()}.svg'
try:
# Create single card content directly with correct template path
typst_content = f'''
#import "templates/single-card.typ": generate-single-card
#generate-single-card(
{number},
side: "front",
bead-shape: "{typst_config['bead_shape']}",
color-scheme: "{typst_config['color_scheme']}",
color-palette: "{typst_config['color_palette']}",
colored-numerals: {str(typst_config['colored_numerals']).lower()},
hide-inactive-beads: {str(typst_config['hide_inactive_beads']).lower()},
show-empty-columns: {str(typst_config['show_empty_columns']).lower()},
columns: {typst_config['columns']},
transparent: {str(typst_config['transparent']).lower()},
width: {typst_config['card_width']},
height: {typst_config['card_height']},
font-size: {typst_config['font_size']},
font-family: "{typst_config['font_family']}",
scale-factor: {typst_config['scale_factor']}
)
'''
with open(temp_typst, 'w') as f:
f.write(typst_content)
# Generate SVG
result = subprocess.run(
['typst', 'compile', str(temp_typst), str(temp_svg), '--format', 'svg'],
capture_output=True,
text=True,
cwd=str(core_package_root)
)
if result.returncode != 0:
return json.dumps({
'error': f'Typst SVG compilation failed: {result.stderr}'
})
# Read SVG content
if not temp_svg.exists():
return json.dumps({
'error': 'SVG file was not generated'
})
with open(temp_svg, 'r', encoding='utf-8') as f:
svg_content = f.read()
return json.dumps({
'pdf': svg_content, # Keep field name for compatibility
'count': 1,
'numbers': [number]
})
except Exception as e:
return json.dumps({'error': f'Single card generation failed: {str(e)}'})
finally:
# Clean up temp files
for temp_file in [temp_typst, temp_svg]:
if temp_file and temp_file.exists():
temp_file.unlink()
if __name__ == '__main__':
# Read JSON from stdin, write JSON to stdout
# This allows clean function-like communication
for line in sys.stdin:
try:
config = json.loads(line.strip())
# Check if this is a single-card generation request
if config.get('mode') == 'single-card':
result = generate_single_card_json(line.strip())
else:
result = generate_flashcards_json(line.strip())
print(result)
sys.stdout.flush()
except Exception as e:
print(json.dumps({'error': str(e)}))
sys.stdout.flush()

60
pnpm-lock.yaml generated
View File

@@ -119,9 +119,6 @@ importers:
'@soroban/abacus-react':
specifier: workspace:*
version: link:../../packages/abacus-react
'@soroban/client':
specifier: workspace:*
version: link:../../packages/core/client/typescript
'@soroban/core':
specifier: workspace:*
version: link:../../packages/core/client/node
@@ -380,49 +377,8 @@ importers:
specifier: ^1.0.0
version: 1.6.1(@types/node@20.19.19)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)
packages/core/client/browser:
dependencies:
'@myriaddreamin/typst-ts-renderer':
specifier: ^0.6.0
version: 0.6.0
'@myriaddreamin/typst-ts-web-compiler':
specifier: ^0.6.0
version: 0.6.0
devDependencies:
typescript:
specifier: ^5.0.0
version: 5.9.3
vite:
specifier: ^5.0.0
version: 5.4.20(@types/node@20.19.19)(terser@5.44.0)
packages/core/client/node:
dependencies:
python-shell:
specifier: ^5.0.0
version: 5.0.0
devDependencies:
'@types/minimatch':
specifier: ^6.0.0
version: 6.0.0
'@types/node':
specifier: ^20.0.0
version: 20.19.19
tsup:
specifier: ^7.0.0
version: 7.3.0(postcss@8.5.6)(typescript@5.9.3)
typescript:
specifier: ^5.0.0
version: 5.9.3
vitest:
specifier: ^1.0.0
version: 1.6.1(@types/node@20.19.19)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)
packages/core/client/typescript:
devDependencies:
'@types/minimatch':
specifier: ^6.0.0
version: 6.0.0
'@types/node':
specifier: ^20.0.0
version: 20.19.19
@@ -1957,12 +1913,6 @@ packages:
'@types/react': '>=16'
react: '>=16'
'@myriaddreamin/typst-ts-renderer@0.6.0':
resolution: {integrity: sha512-56Mids4E5Ob6LeEeXDedvmsVnEWnLmc1qeUOeUSruL/zI3S9QXleF/c3Os1FXwJmLuCFbWTEIq8Quh2cXlnxKw==}
'@myriaddreamin/typst-ts-web-compiler@0.6.0':
resolution: {integrity: sha512-P/eIJ5RnfElj0NYzn5PI296t/IwWtgqUyyTMi5Jm5X3V5kZfskkH+LI7mSQe8tEyxwgCvxbxvFe5adinA3K8Gg==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -7773,10 +7723,6 @@ packages:
python-bridge@1.1.0:
resolution: {integrity: sha512-qjQ0QB8p9cn/XDeILQH0aP307hV58lrmv0Opjyub68Um7FHdF+ZXlTqyxNkKaXOFk2QSkScoPWwn7U9GGnrkeQ==}
python-shell@5.0.0:
resolution: {integrity: sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==}
engines: {node: '>=0.10'}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -10814,10 +10760,6 @@ snapshots:
'@types/react': 18.3.26
react: 18.3.1
'@myriaddreamin/typst-ts-renderer@0.6.0': {}
'@myriaddreamin/typst-ts-web-compiler@0.6.0': {}
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.5.0
@@ -17642,8 +17584,6 @@ snapshots:
dependencies:
bluebird: 3.7.2
python-shell@5.0.0: {}
qs@6.13.0:
dependencies:
side-channel: 1.1.0