mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Update build scripts and enhance README: Modify build command to delete HTML files before building, improve search indexing command, and add a badge for DeepWiki in the README. Refactor math subject initialization and enhance type definitions in BigNumbersScale component for better clarity and maintainability. Introduce sound effects in vertical arithmetic workbench for improved user interaction.
This commit is contained in:
parent
27e802777f
commit
3f3ea4a36c
14 changed files with 180 additions and 25 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
# Open Free Books
|
# Open Free Books
|
||||||
|
|
||||||
|
[](https://deepwiki.com/HananoshikaYomaru/openfreebooks)
|
||||||
|
|
||||||
> Work in Progress. I am looking for contributors to help build this project. Educators and engineer welcome.
|
> Work in Progress. I am looking for contributors to help build this project. Educators and engineer welcome.
|
||||||
|
|
||||||
Free, open-source textbooks for every learner — from elementary school through university. The site is fully static, open on GitHub, and deployed to Cloudflare.
|
Free, open-source textbooks for every learner — from elementary school through university. The site is fully static, open on GitHub, and deployed to Cloudflare.
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,33 @@ function BigNumbersScale() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const canvasCtx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!canvasCtx) return;
|
||||||
|
const ctx: CanvasRenderingContext2D = canvasCtx;
|
||||||
|
|
||||||
const STEPS = [
|
type CoreTone = "green" | "purple" | "orange" | "yellow" | "cyan";
|
||||||
|
type CoreColorInfo = {
|
||||||
|
hex: string;
|
||||||
|
rgb: [number, number, number];
|
||||||
|
glow: string;
|
||||||
|
};
|
||||||
|
type StepMeta = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
power: string;
|
||||||
|
tone: CoreTone;
|
||||||
|
example: string;
|
||||||
|
rawTime: number;
|
||||||
|
};
|
||||||
|
type TierMeta = {
|
||||||
|
index: number;
|
||||||
|
unitValue: number;
|
||||||
|
colorName: CoreTone;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEPS: StepMeta[] = [
|
||||||
{ id: "one", label: "1", value: 1, power: "10⁰", tone: "green", example: "A single heartbeat. The primary unit of all structures.", rawTime: 1 },
|
{ id: "one", label: "1", value: 1, power: "10⁰", tone: "green", example: "A single heartbeat. The primary unit of all structures.", rawTime: 1 },
|
||||||
{ id: "ten", label: "10", value: 10, power: "10¹", tone: "green", example: "The fingers on two hands. The basic structural unit of decimal arithmetic.", rawTime: 10 },
|
{ id: "ten", label: "10", value: 10, power: "10¹", tone: "green", example: "The fingers on two hands. The basic structural unit of decimal arithmetic.", rawTime: 10 },
|
||||||
{ id: "hundred", label: "100", value: 100, power: "10²", tone: "purple", example: "Grains of salt in a pinch. Page count of a short story.", rawTime: 100 },
|
{ id: "hundred", label: "100", value: 100, power: "10²", tone: "purple", example: "Grains of salt in a pinch. Page count of a short story.", rawTime: 100 },
|
||||||
|
|
@ -68,23 +91,23 @@ function BigNumbersScale() {
|
||||||
{ id: "ten_million", label: "10 Million", value: 10_000_000, power: "10⁷", tone: "yellow", example: "Population of a major mega-city like Tokyo, Seoul, or London.", rawTime: 10_000_000 },
|
{ id: "ten_million", label: "10 Million", value: 10_000_000, power: "10⁷", tone: "yellow", example: "Population of a major mega-city like Tokyo, Seoul, or London.", rawTime: 10_000_000 },
|
||||||
{ id: "hundred_million", label: "100 Million", value: 100_000_000, power: "10⁸", tone: "cyan", example: "Total books published in human history. Heartbeats of a cat over its lifetime.", rawTime: 100_000_000 },
|
{ id: "hundred_million", label: "100 Million", value: 100_000_000, power: "10⁸", tone: "cyan", example: "Total books published in human history. Heartbeats of a cat over its lifetime.", rawTime: 100_000_000 },
|
||||||
{ id: "billion", label: "1 Billion", value: 1_000_000_000, power: "10⁹", tone: "cyan", example: "Grains of sand in a typical children's playground. Light travel distance in meters in 3.3 seconds.", rawTime: 1_000_000_000 },
|
{ id: "billion", label: "1 Billion", value: 1_000_000_000, power: "10⁹", tone: "cyan", example: "Grains of sand in a typical children's playground. Light travel distance in meters in 3.3 seconds.", rawTime: 1_000_000_000 },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
const CORES = {
|
const CORES: Record<CoreTone, CoreColorInfo> = {
|
||||||
green: { hex: "#14b8a6", rgb: [20, 184, 166] as [number, number, number], glow: "rgba(20, 184, 166, 0.4)" },
|
green: { hex: "#14b8a6", rgb: [20, 184, 166] as [number, number, number], glow: "rgba(20, 184, 166, 0.4)" },
|
||||||
purple: { hex: "#8b5cf6", rgb: [139, 92, 246] as [number, number, number], glow: "rgba(139, 92, 246, 0.4)" },
|
purple: { hex: "#8b5cf6", rgb: [139, 92, 246] as [number, number, number], glow: "rgba(139, 92, 246, 0.4)" },
|
||||||
orange: { hex: "#f97316", rgb: [249, 115, 22] as [number, number, number], glow: "rgba(249, 115, 22, 0.4)" },
|
orange: { hex: "#f97316", rgb: [249, 115, 22] as [number, number, number], glow: "rgba(249, 115, 22, 0.4)" },
|
||||||
yellow: { hex: "#f59e0b", rgb: [245, 158, 11] as [number, number, number], glow: "rgba(245, 158, 11, 0.4)" },
|
yellow: { hex: "#f59e0b", rgb: [245, 158, 11] as [number, number, number], glow: "rgba(245, 158, 11, 0.4)" },
|
||||||
cyan: { hex: "#06b6d4", rgb: [6, 182, 212] as [number, number, number], glow: "rgba(6, 182, 212, 0.4)" },
|
cyan: { hex: "#06b6d4", rgb: [6, 182, 212] as [number, number, number], glow: "rgba(6, 182, 212, 0.4)" },
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
const TIERS = [
|
const TIERS: TierMeta[] = [
|
||||||
{ index: 0, unitValue: 1, colorName: "green", label: "Units (Green)" },
|
{ index: 0, unitValue: 1, colorName: "green", label: "Units (Green)" },
|
||||||
{ index: 1, unitValue: 100, colorName: "purple", label: "Hundreds (Purple)" },
|
{ index: 1, unitValue: 100, colorName: "purple", label: "Hundreds (Purple)" },
|
||||||
{ index: 2, unitValue: 10_000, colorName: "orange", label: "Ten-Thousands (Orange)" },
|
{ index: 2, unitValue: 10_000, colorName: "orange", label: "Ten-Thousands (Orange)" },
|
||||||
{ index: 3, unitValue: 1_000_000, colorName: "yellow", label: "Millions (Yellow)" },
|
{ index: 3, unitValue: 1_000_000, colorName: "yellow", label: "Millions (Yellow)" },
|
||||||
{ index: 4, unitValue: 100_000_000, colorName: "cyan", label: "Hundred-Millions (Cyan)" },
|
{ index: 4, unitValue: 100_000_000, colorName: "cyan", label: "Hundred-Millions (Cyan)" },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
type VisualMode = "NORMAL" | "COMPRESSING" | "EXPANDING";
|
type VisualMode = "NORMAL" | "COMPRESSING" | "EXPANDING";
|
||||||
type Speed = "slow" | "normal" | "fast";
|
type Speed = "slow" | "normal" | "fast";
|
||||||
|
|
@ -230,7 +253,7 @@ function BigNumbersScale() {
|
||||||
y = 0;
|
y = 0;
|
||||||
radius = 5;
|
radius = 5;
|
||||||
alpha = 1;
|
alpha = 1;
|
||||||
colorInfo = CORES.green;
|
colorInfo: CoreColorInfo = CORES.green;
|
||||||
|
|
||||||
constructor(index: number, tierIdx: number) {
|
constructor(index: number, tierIdx: number) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
|
|
@ -652,7 +675,7 @@ function BigNumbersScale() {
|
||||||
});
|
});
|
||||||
|
|
||||||
btnPrevStep.addEventListener("click", () => {
|
btnPrevStep.addEventListener("click", () => {
|
||||||
let closestSmallerStep = STEPS[0]!.value;
|
let closestSmallerStep: number = STEPS[0]!.value;
|
||||||
for (let i = STEPS.length - 1; i >= 0; i -= 1) {
|
for (let i = STEPS.length - 1; i >= 0; i -= 1) {
|
||||||
if (STEPS[i]!.value < state.targetValue) {
|
if (STEPS[i]!.value < state.targetValue) {
|
||||||
closestSmallerStep = STEPS[i]!.value;
|
closestSmallerStep = STEPS[i]!.value;
|
||||||
|
|
@ -664,7 +687,7 @@ function BigNumbersScale() {
|
||||||
});
|
});
|
||||||
|
|
||||||
btnNextStep.addEventListener("click", () => {
|
btnNextStep.addEventListener("click", () => {
|
||||||
let closestLargerStep = STEPS[STEPS.length - 1]!.value;
|
let closestLargerStep: number = STEPS[STEPS.length - 1]!.value;
|
||||||
for (let i = 0; i < STEPS.length; i += 1) {
|
for (let i = 0; i < STEPS.length; i += 1) {
|
||||||
if (STEPS[i]!.value > state.targetValue) {
|
if (STEPS[i]!.value > state.targetValue) {
|
||||||
closestLargerStep = STEPS[i]!.value;
|
closestLargerStep = STEPS[i]!.value;
|
||||||
|
|
|
||||||
|
|
@ -496,12 +496,10 @@ function SimultaneousSolverWorkbench() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={current() && !error()}>
|
<Show when={current() && !error()}>
|
||||||
{(step) => (
|
<div class="sim-solver__step-panel">
|
||||||
<div class="sim-solver__step-panel">
|
<h3 class="sim-solver__step-title">{current()!.title}</h3>
|
||||||
<h3 class="sim-solver__step-title">{step().title}</h3>
|
<div class="sim-solver__step-math" innerHTML={renderLatex(current()!.latex)} />
|
||||||
<div class="sim-solver__step-math" innerHTML={renderLatex(step().latex)} />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,5 @@ export function initMathSubject(root: HTMLElement): void {
|
||||||
panel.classList.add("book-question__solution");
|
panel.classList.add("book-question__solution");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initSubject = initMathSubject;
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@
|
||||||
.va-workbench__brand {
|
.va-workbench__brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +106,24 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.va-workbench__sound-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.35rem 0.55rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--paper) 55%, var(--card-bg));
|
||||||
|
color: var(--soft-ink);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.va-workbench__sound-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.va-workbench__field {
|
.va-workbench__field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,82 @@ type Op = "+" | "-" | "*" | "/";
|
||||||
|
|
||||||
const DEFAULT_NUMBERS = "27\n59";
|
const DEFAULT_NUMBERS = "27\n59";
|
||||||
|
|
||||||
|
class WorkbenchSound {
|
||||||
|
private ctx: AudioContext | null = null;
|
||||||
|
muted = true;
|
||||||
|
private lastTick = 0;
|
||||||
|
|
||||||
|
private ensureCtx() {
|
||||||
|
if (!this.ctx) {
|
||||||
|
this.ctx = new (
|
||||||
|
window.AudioContext ||
|
||||||
|
(window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
|
||||||
|
)();
|
||||||
|
}
|
||||||
|
if (this.ctx.state === "suspended") {
|
||||||
|
void this.ctx.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMute() {
|
||||||
|
this.ensureCtx();
|
||||||
|
this.muted = !this.muted;
|
||||||
|
return this.muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private play(
|
||||||
|
type: OscillatorType,
|
||||||
|
fromHz: number,
|
||||||
|
toHz: number,
|
||||||
|
duration: number,
|
||||||
|
gainStart: number
|
||||||
|
) {
|
||||||
|
if (this.muted) return;
|
||||||
|
this.ensureCtx();
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.ctx.destination);
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.setValueAtTime(fromHz, this.ctx.currentTime);
|
||||||
|
if (toHz > fromHz) {
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(toHz, this.ctx.currentTime + duration);
|
||||||
|
} else {
|
||||||
|
osc.frequency.linearRampToValueAtTime(toHz, this.ctx.currentTime + duration);
|
||||||
|
}
|
||||||
|
gain.gain.setValueAtTime(gainStart, this.ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this.ctx.currentTime + duration + 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
playTick() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const now = this.ctx.currentTime;
|
||||||
|
if (now - this.lastTick < 0.04) return;
|
||||||
|
this.lastTick = now;
|
||||||
|
this.play("sine", 520, 220, 0.07, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
playGenerate() {
|
||||||
|
this.play("triangle", 200, 420, 0.22, 0.11);
|
||||||
|
}
|
||||||
|
|
||||||
|
playTogglePlay(playing: boolean) {
|
||||||
|
if (playing) {
|
||||||
|
this.play("triangle", 260, 600, 0.15, 0.09);
|
||||||
|
} else {
|
||||||
|
this.play("sawtooth", 440, 220, 0.12, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playError() {
|
||||||
|
this.play("square", 180, 120, 0.16, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function VerticalArithmeticWorkbench() {
|
function VerticalArithmeticWorkbench() {
|
||||||
let rootRef!: HTMLElement;
|
let rootRef!: HTMLElement;
|
||||||
let canvasRef!: HTMLCanvasElement;
|
let canvasRef!: HTMLCanvasElement;
|
||||||
|
|
@ -19,6 +95,11 @@ function VerticalArithmeticWorkbench() {
|
||||||
const [stepTotal, setStepTotal] = createSignal(0);
|
const [stepTotal, setStepTotal] = createSignal(0);
|
||||||
const [playing, setPlaying] = createSignal(false);
|
const [playing, setPlaying] = createSignal(false);
|
||||||
const [speed, setSpeed] = createSignal(800);
|
const [speed, setSpeed] = createSignal(800);
|
||||||
|
const [soundMuted, setSoundMuted] = createSignal(true);
|
||||||
|
|
||||||
|
const sound = new WorkbenchSound();
|
||||||
|
let lastStep = 0;
|
||||||
|
let lastPlaying = false;
|
||||||
|
|
||||||
const progress = () =>
|
const progress = () =>
|
||||||
stepTotal() > 0 ? (stepCurrent() / stepTotal()) * 100 : 0;
|
stepTotal() > 0 ? (stepCurrent() / stepTotal()) * 100 : 0;
|
||||||
|
|
@ -32,12 +113,23 @@ function VerticalArithmeticWorkbench() {
|
||||||
visualizer.onMessage = (msg, err) => {
|
visualizer.onMessage = (msg, err) => {
|
||||||
setMessage(msg);
|
setMessage(msg);
|
||||||
setIsError(!!err);
|
setIsError(!!err);
|
||||||
|
if (err) sound.playError();
|
||||||
};
|
};
|
||||||
visualizer.onStep = (current, total) => {
|
visualizer.onStep = (current, total) => {
|
||||||
setStepCurrent(current);
|
setStepCurrent(current);
|
||||||
setStepTotal(total);
|
setStepTotal(total);
|
||||||
|
if (current !== lastStep) {
|
||||||
|
sound.playTick();
|
||||||
|
}
|
||||||
|
lastStep = current;
|
||||||
|
};
|
||||||
|
visualizer.onPlayState = (nextPlaying) => {
|
||||||
|
setPlaying(nextPlaying);
|
||||||
|
if (nextPlaying !== lastPlaying) {
|
||||||
|
sound.playTogglePlay(nextPlaying);
|
||||||
|
}
|
||||||
|
lastPlaying = nextPlaying;
|
||||||
};
|
};
|
||||||
visualizer.onPlayState = setPlaying;
|
|
||||||
|
|
||||||
const resize = () => visualizer?.resizeCanvas();
|
const resize = () => visualizer?.resizeCanvas();
|
||||||
resize();
|
resize();
|
||||||
|
|
@ -72,6 +164,7 @@ function VerticalArithmeticWorkbench() {
|
||||||
const generate = () => {
|
const generate = () => {
|
||||||
visualizer?.setOperation(operation());
|
visualizer?.setOperation(operation());
|
||||||
visualizer?.generateFromInput(numbersInput());
|
visualizer?.generateFromInput(numbersInput());
|
||||||
|
sound.playGenerate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -93,6 +186,18 @@ function VerticalArithmeticWorkbench() {
|
||||||
<span class="va-workbench__brand-text">
|
<span class="va-workbench__brand-text">
|
||||||
Arithmetic <strong>Visualizer</strong>
|
Arithmetic <strong>Visualizer</strong>
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="va-workbench__sound-btn"
|
||||||
|
aria-pressed={!soundMuted()}
|
||||||
|
title={soundMuted() ? "Enable sound effects" : "Mute sound effects"}
|
||||||
|
onClick={() => {
|
||||||
|
const muted = sound.toggleMute();
|
||||||
|
setSoundMuted(muted);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{soundMuted() ? "🔇 Muted" : "🔊 Active"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="va-workbench__field">
|
<div class="va-workbench__field">
|
||||||
|
|
@ -112,7 +217,10 @@ function VerticalArithmeticWorkbench() {
|
||||||
classList={{ "va-workbench__op-btn--active": operation() === op }}
|
classList={{ "va-workbench__op-btn--active": operation() === op }}
|
||||||
aria-pressed={operation() === op}
|
aria-pressed={operation() === op}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onClick={() => setOperation(op)}
|
onClick={() => {
|
||||||
|
setOperation(op);
|
||||||
|
sound.playTick();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{op === "*" ? "×" : op === "/" ? "÷" : op}
|
{op === "*" ? "×" : op === "/" ? "÷" : op}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ function mountChapterWidgets() {
|
||||||
}
|
}
|
||||||
loadPromises.push(
|
loadPromises.push(
|
||||||
loader().then((mod) => {
|
loader().then((mod) => {
|
||||||
render(() => mod.default(), el);
|
render(() => mod.default({}), el);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sync:chapters": "bun scripts/sync-chapters.ts",
|
"sync:chapters": "bun scripts/sync-chapters.ts",
|
||||||
"build:js": "bun run sync:chapters && vite build",
|
"build:js": "bun run sync:chapters && vite build",
|
||||||
"build": "rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search",
|
"build": "find themes/openfreebooks/static -name '*.html' -delete && rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search",
|
||||||
"index:search": "pagefind --site public && rm -rf static/pagefind && cp -R public/pagefind static/pagefind",
|
"index:search": "pagefind --site public --glob \"**/index.html\" && rm -rf static/pagefind && cp -R public/pagefind static/pagefind",
|
||||||
"dev:js": "bun run sync:chapters && vite build --watch",
|
"dev:js": "bun run sync:chapters && vite build --watch",
|
||||||
"scan:size": "bun scripts/scan-output-size.ts",
|
"scan:size": "bun scripts/scan-output-size.ts",
|
||||||
"test": "bun test frontend/src/lib"
|
"test": "bun test frontend/src/lib"
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ function buildSubjectModuleRegistry(
|
||||||
];
|
];
|
||||||
for (const { id, dir } of withJs) {
|
for (const { id, dir } of withJs) {
|
||||||
const relFromGenerated = relative(dirname(GENERATED_SUBJECT_MODULES), dir).replace(/\\/g, "/");
|
const relFromGenerated = relative(dirname(GENERATED_SUBJECT_MODULES), dir).replace(/\\/g, "/");
|
||||||
lines.push(` "${id}": () => import("${relFromGenerated}/subject.ts"),`);
|
lines.push(` "${id}": () => import("${relFromGenerated}/subject"),`);
|
||||||
}
|
}
|
||||||
lines.push("};", "");
|
lines.push("};", "");
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
|
|
@ -314,7 +314,7 @@ function buildWidgetRegistry(
|
||||||
for (const { key, widgets, dir } of entries) {
|
for (const { key, widgets, dir } of entries) {
|
||||||
const relFromGenerated = relative(dirname(GENERATED_WIDGETS), dir).replace(/\\/g, "/");
|
const relFromGenerated = relative(dirname(GENERATED_WIDGETS), dir).replace(/\\/g, "/");
|
||||||
for (const widget of widgets) {
|
for (const widget of widgets) {
|
||||||
const importPath = `${relFromGenerated}/widgets/${widget}.tsx`;
|
const importPath = `${relFromGenerated}/widgets/${widget}`;
|
||||||
lines.push(` "${key}/${widget}": () => import("${importPath}"),`);
|
lines.push(` "${key}/${widget}": () => import("${importPath}"),`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
spec.md
2
spec.md
|
|
@ -18,6 +18,8 @@ we need to have a question bank feature. since this is purely static sites, ques
|
||||||
|
|
||||||
question bank not only show answer but also steps, explanation, calcultion.
|
question bank not only show answer but also steps, explanation, calcultion.
|
||||||
|
|
||||||
|
best practice for creating chapters: correctness > completeness > order and structure > accessibility > optimization
|
||||||
|
|
||||||
## Chapters requirements
|
## Chapters requirements
|
||||||
|
|
||||||
### Maths
|
### Maths
|
||||||
|
|
|
||||||
BIN
static/og.jpg
Normal file
BIN
static/og.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
static/og.png
BIN
static/og.png
Binary file not shown.
|
Before Width: | Height: | Size: 854 KiB |
|
|
@ -1,5 +1,5 @@
|
||||||
{# Open Graph + Twitter Card — shared site image on every page #}
|
{# Open Graph + Twitter Card — shared site image on every page #}
|
||||||
{% set og_image = get_url(path='og.png', trailing_slash=false) %}
|
{% set og_image = get_url(path='og.jpg', trailing_slash=false) %}
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="{{ config.extra.site_title }}" />
|
<meta property="og:site_name" content="{{ config.extra.site_title }}" />
|
||||||
<meta property="og:title" content="{% if page %}{{ page.title }}{% elif section %}{{ section.title }}{% else %}{{ config.title }}{% endif %}" />
|
<meta property="og:title" content="{% if page %}{{ page.title }}{% elif section %}{{ section.title }}{% else %}{{ config.title }}{% endif %}" />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solid()],
|
plugins: [solid()],
|
||||||
|
publicDir: false,
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@ofb/katex": path.resolve(rootDir, "frontend/src/lib/katex.ts"),
|
"@ofb/katex": path.resolve(rootDir, "frontend/src/lib/katex.ts"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue