feat: add React card renderer and interactive UML
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
examples/card/build/
|
examples/card/build/
|
||||||
examples/card/web/
|
examples/card/web/
|
||||||
|
|||||||
Generated
+605
@@ -0,0 +1,605 @@
|
|||||||
|
{
|
||||||
|
"name": "stem-card-layouts-react-renderer",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "stem-card-layouts-react-renderer",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"esbuild": "^0.25.6",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||||
|
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.25.12",
|
||||||
|
"@esbuild/android-arm": "0.25.12",
|
||||||
|
"@esbuild/android-arm64": "0.25.12",
|
||||||
|
"@esbuild/android-x64": "0.25.12",
|
||||||
|
"@esbuild/darwin-arm64": "0.25.12",
|
||||||
|
"@esbuild/darwin-x64": "0.25.12",
|
||||||
|
"@esbuild/freebsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/freebsd-x64": "0.25.12",
|
||||||
|
"@esbuild/linux-arm": "0.25.12",
|
||||||
|
"@esbuild/linux-arm64": "0.25.12",
|
||||||
|
"@esbuild/linux-ia32": "0.25.12",
|
||||||
|
"@esbuild/linux-loong64": "0.25.12",
|
||||||
|
"@esbuild/linux-mips64el": "0.25.12",
|
||||||
|
"@esbuild/linux-ppc64": "0.25.12",
|
||||||
|
"@esbuild/linux-riscv64": "0.25.12",
|
||||||
|
"@esbuild/linux-s390x": "0.25.12",
|
||||||
|
"@esbuild/linux-x64": "0.25.12",
|
||||||
|
"@esbuild/netbsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/netbsd-x64": "0.25.12",
|
||||||
|
"@esbuild/openbsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/openbsd-x64": "0.25.12",
|
||||||
|
"@esbuild/openharmony-arm64": "0.25.12",
|
||||||
|
"@esbuild/sunos-x64": "0.25.12",
|
||||||
|
"@esbuild/win32-arm64": "0.25.12",
|
||||||
|
"@esbuild/win32-ia32": "0.25.12",
|
||||||
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "stem-card-layouts-react-renderer",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build:react": "node react/build.mjs",
|
||||||
|
"typecheck:react": "tsc --noEmit -p react/tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"esbuild": "^0.25.6",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { build } from "esbuild";
|
||||||
|
|
||||||
|
const outputDirectory = path.resolve(process.argv[2] ?? "web");
|
||||||
|
const reactDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entryPoints: [path.join(reactDirectory, "src/main.tsx")],
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
platform: "browser",
|
||||||
|
target: ["es2022"],
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false,
|
||||||
|
define: { "process.env.NODE_ENV": '"production"' },
|
||||||
|
outfile: path.join(outputDirectory, "app.js"),
|
||||||
|
loader: { ".tsx": "tsx" },
|
||||||
|
});
|
||||||
@@ -0,0 +1,729 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./react.css";
|
||||||
|
|
||||||
|
type Status = "not_started" | "discussed" | "completed";
|
||||||
|
type InteractiveArea = "compile" | "bss" | "stack" | "registers" | "text";
|
||||||
|
type InteractiveSymbol = {
|
||||||
|
id: string;
|
||||||
|
area: InteractiveArea;
|
||||||
|
declaration: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
type UmlStage = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
code_ref?: string;
|
||||||
|
};
|
||||||
|
type InteractiveAsset = {
|
||||||
|
kind: "allocator-memory-flow" | "uml-sequence";
|
||||||
|
storage_key?: string;
|
||||||
|
arena_size?: number;
|
||||||
|
allocations?: number[];
|
||||||
|
symbols?: InteractiveSymbol[];
|
||||||
|
stages?: UmlStage[];
|
||||||
|
};
|
||||||
|
type Asset = {
|
||||||
|
label: string;
|
||||||
|
caption: string;
|
||||||
|
alt: string;
|
||||||
|
href: string;
|
||||||
|
width: number;
|
||||||
|
kind: string;
|
||||||
|
source_href?: string;
|
||||||
|
interactive?: InteractiveAsset | null;
|
||||||
|
};
|
||||||
|
type FlowStep = { id: string; title: string; content_html: string };
|
||||||
|
type FlowBlock = {
|
||||||
|
id: string;
|
||||||
|
kind: "block" | "exercise";
|
||||||
|
title: string;
|
||||||
|
content_html: string;
|
||||||
|
steps: FlowStep[];
|
||||||
|
};
|
||||||
|
type Task = {
|
||||||
|
ref: string;
|
||||||
|
title: string;
|
||||||
|
uuid?: string;
|
||||||
|
criterion: string;
|
||||||
|
prompt_html?: string;
|
||||||
|
flow: FlowBlock[];
|
||||||
|
};
|
||||||
|
type Section = {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
title: string;
|
||||||
|
header_html: string;
|
||||||
|
left_margin_html: string;
|
||||||
|
right_margin_html: string;
|
||||||
|
content_html: string;
|
||||||
|
tasks: Task[];
|
||||||
|
assets: Asset[];
|
||||||
|
steps: { id: string; title: string }[];
|
||||||
|
};
|
||||||
|
type Progress = {
|
||||||
|
items: Record<string, { status: Status }>;
|
||||||
|
summary: { total: number; counts: Record<Status, number> };
|
||||||
|
};
|
||||||
|
type CardModel = {
|
||||||
|
schema: string;
|
||||||
|
card: Record<string, string>;
|
||||||
|
total_pages: number;
|
||||||
|
front: any;
|
||||||
|
sections: Section[];
|
||||||
|
dictionary: { header_html: string; content_html: string };
|
||||||
|
toc: { id: string; label: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Html = ({ html, className }: { html: string; className?: string }) => (
|
||||||
|
<div className={className} dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
function Header({ html }: { html: string }) {
|
||||||
|
return <Html html={html} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Margin({ side, html }: { side: "left" | "right"; html: string }) {
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={`pdf-margin ${side}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sheet({
|
||||||
|
header,
|
||||||
|
left = "",
|
||||||
|
right = "",
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
}: React.PropsWithChildren<any>) {
|
||||||
|
return (
|
||||||
|
<article className={`sheet ${className}`}>
|
||||||
|
<div className="sheet-content">
|
||||||
|
<Header html={header} />
|
||||||
|
<Margin side="left" html={left} />
|
||||||
|
<section className="pdf-main card-section">{children}</section>
|
||||||
|
<Margin side="right" html={right} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Topbar({
|
||||||
|
toc,
|
||||||
|
scale,
|
||||||
|
setScale,
|
||||||
|
}: {
|
||||||
|
toc: CardModel["toc"];
|
||||||
|
scale: number;
|
||||||
|
setScale: (value: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<nav className="topbar">
|
||||||
|
<div className="topbar-inner">
|
||||||
|
<details>
|
||||||
|
<summary>Spis treści</summary>
|
||||||
|
<div className="toc-links">
|
||||||
|
{toc.map((item) => (
|
||||||
|
<a key={item.id} href={`#${item.id}`}>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div className="viewer-zoom-controls">
|
||||||
|
<output className="viewer-zoom-status">
|
||||||
|
Płótno {Math.round(scale * 100)}% · Treść 100%
|
||||||
|
</output>
|
||||||
|
<button className="viewer-zoom-reset" onClick={() => setScale(2.18)}>
|
||||||
|
Reset zoom
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FrontPage({ front }: { front: any }) {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
header={front.header_html}
|
||||||
|
left={front.left_margin_html}
|
||||||
|
right={front.right_margin_html}
|
||||||
|
className="front-sheet"
|
||||||
|
>
|
||||||
|
<section className="front-scope front-goal">
|
||||||
|
<h2>{front.goal_title}</h2>
|
||||||
|
<Html html={front.goal_html} />
|
||||||
|
</section>
|
||||||
|
<section className="front-scope front-card-scope">
|
||||||
|
<h2>{front.scope_title}</h2>
|
||||||
|
{front.scope_html && <Html html={front.scope_html} />}
|
||||||
|
<div className="scope-table-wrap">
|
||||||
|
<table className="scope-table scope-table--task">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{front.scope_headers.map((header: string) => (
|
||||||
|
<th key={header}>{header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{front.scope_rows.map((row: any) => (
|
||||||
|
<tr
|
||||||
|
key={`${row.chapter}-${row.task}`}
|
||||||
|
className={row.key ? "scope-row--key" : ""}
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<code>{row.chapter}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>{row.task}</code>
|
||||||
|
</td>
|
||||||
|
<td dangerouslySetInnerHTML={{ __html: row.idea_html }} />
|
||||||
|
<td>{row.priority}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskStep({ taskRef, step }: { taskRef: string; step: FlowStep }) {
|
||||||
|
return (
|
||||||
|
<li id={`${taskRef}-${step.id}`}>
|
||||||
|
<strong>{step.title}</strong>
|
||||||
|
<Html html={step.content_html} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskBlock({ taskRef, block }: { taskRef: string; block: FlowBlock }) {
|
||||||
|
const label = block.kind === "exercise" ? "Ćwiczenie" : "Block";
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`task-block task-block--${block.kind}`}
|
||||||
|
id={`${taskRef}-${block.id}`}
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<span>{label}</span>
|
||||||
|
<h4>{block.title}</h4>
|
||||||
|
</header>
|
||||||
|
<Html html={block.content_html} />
|
||||||
|
{block.steps.length > 0 && (
|
||||||
|
<ol className="block-steps">
|
||||||
|
{block.steps.map((step) => (
|
||||||
|
<TaskStep key={step.id} taskRef={taskRef} step={step} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskFlow({ task }: { task: Task }) {
|
||||||
|
if (!task.flow.length)
|
||||||
|
return (
|
||||||
|
<article className="task-legacy" id={task.ref}>
|
||||||
|
<strong>{task.ref.toUpperCase()}</strong>
|
||||||
|
<Html html={task.prompt_html ?? ""} />
|
||||||
|
<footer>
|
||||||
|
<strong>Task acceptance:</strong> {task.criterion}
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<article className="task-flow" id={task.ref}>
|
||||||
|
<header className="task-flow-header">
|
||||||
|
<span>{task.ref.toUpperCase()}</span>
|
||||||
|
<h3>{task.title}</h3>
|
||||||
|
{task.uuid && <code>{task.uuid}</code>}
|
||||||
|
</header>
|
||||||
|
{task.flow.map((block) => (
|
||||||
|
<TaskBlock key={block.id} taskRef={task.ref} block={block} />
|
||||||
|
))}
|
||||||
|
<footer>
|
||||||
|
<strong>Task acceptance:</strong> {task.criterion}
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaLabels: Record<InteractiveArea, string> = {
|
||||||
|
compile: "KOMPILACJA",
|
||||||
|
bss: ".BSS",
|
||||||
|
stack: "STOS",
|
||||||
|
registers: "REJESTRY",
|
||||||
|
text: ".TEXT",
|
||||||
|
};
|
||||||
|
const allocationColors = ["#9bcfe2", "#c4dfc0", "#f4c889", "#d2b6dc"];
|
||||||
|
|
||||||
|
function InteractiveMemoryFlow({
|
||||||
|
config,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
config: InteractiveAsset;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
const arenaSize = config.arena_size ?? 64;
|
||||||
|
const allocations = config.allocations ?? [5, 7];
|
||||||
|
const symbols = config.symbols ?? [];
|
||||||
|
const storageKey = `${config.storage_key ?? "allocator-memory-flow"}-stage`;
|
||||||
|
const [stage, setStage] = useState(() =>
|
||||||
|
Math.min(allocations.length, Number(localStorage.getItem(storageKey) ?? 0)),
|
||||||
|
);
|
||||||
|
const [focus, setFocus] = useState(symbols[0]?.id ?? "");
|
||||||
|
useEffect(
|
||||||
|
() => localStorage.setItem(storageKey, String(stage)),
|
||||||
|
[stage, storageKey],
|
||||||
|
);
|
||||||
|
const used = allocations.slice(0, stage).reduce((sum, size) => sum + size, 0);
|
||||||
|
const focused = symbols.find((symbol) => symbol.id === focus) ?? symbols[0];
|
||||||
|
const cells = Array.from({ length: arenaSize }, (_, index) => index);
|
||||||
|
const allocationIndex = (cell: number) => {
|
||||||
|
let end = 0;
|
||||||
|
for (let index = 0; index < stage; index += 1) {
|
||||||
|
end += allocations[index];
|
||||||
|
if (cell < end) return index;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
const stageText =
|
||||||
|
stage === 0
|
||||||
|
? "allocator_reset(): allocp = allocbuf"
|
||||||
|
: `alloc_local(${allocations[stage - 1]}): allocp = allocbuf + ${used}`;
|
||||||
|
const areas = (Object.keys(areaLabels) as InteractiveArea[]).filter((area) =>
|
||||||
|
symbols.some((symbol) => symbol.area === area),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="memory-react" aria-label={`Interaktywny diagram: ${title}`}>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<small>INTERAKTYWNY MODEL PAMIĘCI</small>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="memory-actions">
|
||||||
|
<button aria-pressed={stage === 0} onClick={() => setStage(0)}>
|
||||||
|
definicje / reset
|
||||||
|
</button>
|
||||||
|
{allocations.map((size, index) => (
|
||||||
|
<button
|
||||||
|
key={`${size}-${index}`}
|
||||||
|
aria-pressed={stage === index + 1}
|
||||||
|
onClick={() => setStage(index + 1)}
|
||||||
|
>
|
||||||
|
alloc({size})
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="memory-lanes">
|
||||||
|
{areas.map((area) => (
|
||||||
|
<section className={`memory-lane ${area}`} key={area}>
|
||||||
|
<b>{areaLabels[area]}</b>
|
||||||
|
{symbols
|
||||||
|
.filter((symbol) => symbol.area === area)
|
||||||
|
.map((symbol) => (
|
||||||
|
<button
|
||||||
|
key={symbol.id}
|
||||||
|
className={focus === symbol.id ? "active" : ""}
|
||||||
|
onClick={() => setFocus(symbol.id)}
|
||||||
|
>
|
||||||
|
<code>{symbol.declaration}</code>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="arena-wrap">
|
||||||
|
<div className="arena-label">
|
||||||
|
<code>allocbuf[{arenaSize}]</code>
|
||||||
|
<span>
|
||||||
|
użyte {used} B / wolne {arenaSize - used} B
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="arena"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--allocp": used,
|
||||||
|
"--arena-size": arenaSize,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cells.map((cell) => {
|
||||||
|
const segment = allocationIndex(cell);
|
||||||
|
return (
|
||||||
|
<i
|
||||||
|
key={cell}
|
||||||
|
style={
|
||||||
|
segment >= 0
|
||||||
|
? {
|
||||||
|
background:
|
||||||
|
allocationColors[segment % allocationColors.length],
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<span className="allocp">allocp ↑ {used}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="memory-stage">
|
||||||
|
<strong>
|
||||||
|
{stage + 1}/{allocations.length + 1}
|
||||||
|
</strong>{" "}
|
||||||
|
<code>{stageText}</code>
|
||||||
|
</p>
|
||||||
|
{focused && (
|
||||||
|
<aside className="memory-inspector">
|
||||||
|
<strong>{focused.id}</strong>
|
||||||
|
<code>{focused.declaration}</code>
|
||||||
|
<span>{focused.description}</span>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteractiveUmlSequence({
|
||||||
|
config,
|
||||||
|
asset,
|
||||||
|
}: {
|
||||||
|
config: InteractiveAsset;
|
||||||
|
asset: Asset;
|
||||||
|
}) {
|
||||||
|
const stages = config.stages ?? [];
|
||||||
|
const storageKey = `${config.storage_key ?? asset.label}-uml-stage`;
|
||||||
|
const [stageIndex, setStageIndex] = useState(() =>
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
stages.length - 1,
|
||||||
|
Number(localStorage.getItem(storageKey) ?? 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
useEffect(
|
||||||
|
() => localStorage.setItem(storageKey, String(stageIndex)),
|
||||||
|
[stageIndex, storageKey],
|
||||||
|
);
|
||||||
|
const stage = stages[stageIndex];
|
||||||
|
return (
|
||||||
|
<div className="uml-react">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<small>KANONICZNY DIAGRAM UML · PLANTUML</small>
|
||||||
|
<h3>Reset i dwa przydziały liniowe</h3>
|
||||||
|
</div>
|
||||||
|
<div className="uml-actions">
|
||||||
|
{stages.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
aria-pressed={stageIndex === index}
|
||||||
|
onClick={() => setStageIndex(index)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<a
|
||||||
|
className="uml-canvas"
|
||||||
|
href={asset.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<img src={asset.href} alt={asset.alt} />
|
||||||
|
</a>
|
||||||
|
{stage && (
|
||||||
|
<aside className="uml-stage">
|
||||||
|
<strong>{stage.label}</strong>
|
||||||
|
<span>{stage.description}</span>
|
||||||
|
{stage.code_ref && <code>{stage.code_ref}</code>}
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AssetFigure({ asset, number }: { asset: Asset; number: number }) {
|
||||||
|
const interactiveKind = asset.interactive?.kind;
|
||||||
|
const isInteractive = Boolean(interactiveKind);
|
||||||
|
return (
|
||||||
|
<figure
|
||||||
|
className={`card-asset asset-${asset.kind} ${isInteractive ? "asset-interactive" : ""}`}
|
||||||
|
id={asset.label.replace(":", "-")}
|
||||||
|
>
|
||||||
|
{interactiveKind === "uml-sequence" ? (
|
||||||
|
<InteractiveUmlSequence config={asset.interactive!} asset={asset} />
|
||||||
|
) : interactiveKind === "allocator-memory-flow" ? (
|
||||||
|
<>
|
||||||
|
<InteractiveMemoryFlow
|
||||||
|
config={asset.interactive!}
|
||||||
|
title={asset.caption}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="asset-static-fallback"
|
||||||
|
src={asset.href}
|
||||||
|
alt={asset.alt}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<a href={asset.href} target="_blank" rel="noopener">
|
||||||
|
<img
|
||||||
|
src={asset.href}
|
||||||
|
alt={asset.alt}
|
||||||
|
loading="lazy"
|
||||||
|
style={{ width: `${asset.width * 100}%` }}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{asset.source_href && (
|
||||||
|
<a
|
||||||
|
className="asset-source-link"
|
||||||
|
href={asset.source_href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
Źródło PlantUML
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<figcaption>
|
||||||
|
Rys. {number}. {asset.caption}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepMap({
|
||||||
|
steps,
|
||||||
|
progress,
|
||||||
|
onCycle,
|
||||||
|
}: {
|
||||||
|
steps: Section["steps"];
|
||||||
|
progress: Progress | null;
|
||||||
|
onCycle: (id: string, status: Status) => void;
|
||||||
|
}) {
|
||||||
|
if (!steps.length) return null;
|
||||||
|
const labels: Record<Status, string> = {
|
||||||
|
not_started: "○ do omówienia",
|
||||||
|
discussed: "◐ omówione",
|
||||||
|
completed: "● wykonane",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="step-map">
|
||||||
|
{steps.map((step) => {
|
||||||
|
const status: Status =
|
||||||
|
progress?.items?.[step.id]?.status ?? "not_started";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`step-row ${status === "completed" ? "lesson-progress-completed" : ""}`}
|
||||||
|
key={step.id}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="lesson-progress-control"
|
||||||
|
data-status={status}
|
||||||
|
onClick={() => onCycle(step.id, status)}
|
||||||
|
disabled={!progress}
|
||||||
|
>
|
||||||
|
{labels[status]}
|
||||||
|
</button>
|
||||||
|
<span className="step-title">{step.title}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionPage({
|
||||||
|
section,
|
||||||
|
progress,
|
||||||
|
onCycle,
|
||||||
|
figureOffset,
|
||||||
|
}: {
|
||||||
|
section: Section;
|
||||||
|
progress: Progress | null;
|
||||||
|
onCycle: (id: string, status: Status) => void;
|
||||||
|
figureOffset: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
header={section.header_html}
|
||||||
|
left={section.left_margin_html}
|
||||||
|
right={section.right_margin_html}
|
||||||
|
className="section-sheet"
|
||||||
|
>
|
||||||
|
<div id={section.id} className="section-anchor" />
|
||||||
|
<h2 className="section-heading">
|
||||||
|
<span className="section-index">{section.index}</span>
|
||||||
|
<span className="section-title">{section.title}</span>
|
||||||
|
</h2>
|
||||||
|
<StepMap steps={section.steps} progress={progress} onCycle={onCycle} />
|
||||||
|
{section.content_html && <Html html={section.content_html} />}
|
||||||
|
<div className="tasks">
|
||||||
|
{section.tasks.map((task) => (
|
||||||
|
<TaskFlow key={task.ref} task={task} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{section.assets.map((asset, index) => (
|
||||||
|
<AssetFigure
|
||||||
|
key={asset.label}
|
||||||
|
asset={asset}
|
||||||
|
number={figureOffset + index + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressDock({
|
||||||
|
progress,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
progress: Progress | null;
|
||||||
|
error: string;
|
||||||
|
}) {
|
||||||
|
if (!progress?.summary && !error) return null;
|
||||||
|
const counts = progress?.summary.counts;
|
||||||
|
return (
|
||||||
|
<aside className="lesson-progress-dock">
|
||||||
|
<strong>Przebieg zajęć</strong>
|
||||||
|
{counts && (
|
||||||
|
<p>
|
||||||
|
Wykonane: {counts.completed}/{progress!.summary.total} · omówione:{" "}
|
||||||
|
{counts.discussed}/{progress!.summary.total}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{error && <p className="lesson-progress-error">{error}</p>}
|
||||||
|
<a href="/api/report/teams.md" target="_blank" rel="noopener">
|
||||||
|
Otwórz raport do Teams
|
||||||
|
</a>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({ model }: { model: CardModel }) {
|
||||||
|
const [scale, setScaleState] = useState(2.18);
|
||||||
|
const [progress, setProgress] = useState<Progress | null>(null);
|
||||||
|
const [progressError, setProgressError] = useState("");
|
||||||
|
const setScale = (value: number) =>
|
||||||
|
setScaleState(Math.max(0.25, Math.min(3, value)));
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--viewer-canvas-scale",
|
||||||
|
scale.toFixed(3),
|
||||||
|
);
|
||||||
|
}, [scale]);
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/progress")
|
||||||
|
.then((response) =>
|
||||||
|
response.ok
|
||||||
|
? response.json()
|
||||||
|
: Promise.reject(new Error(`HTTP ${response.status}`)),
|
||||||
|
)
|
||||||
|
.then((data) => setProgress(data.progress))
|
||||||
|
.catch((error) =>
|
||||||
|
setProgressError(
|
||||||
|
`Postęp działa tylko przez serwer karty (${error.message}).`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
const wheel = (event: WheelEvent) => {
|
||||||
|
if (!event.altKey || event.ctrlKey) return;
|
||||||
|
event.preventDefault();
|
||||||
|
setScaleState((current) =>
|
||||||
|
Math.max(
|
||||||
|
0.25,
|
||||||
|
Math.min(
|
||||||
|
3,
|
||||||
|
current *
|
||||||
|
Math.exp(-Math.max(-120, Math.min(120, event.deltaY)) * 0.001),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
window.addEventListener("wheel", wheel, { passive: false });
|
||||||
|
return () => window.removeEventListener("wheel", wheel);
|
||||||
|
}, []);
|
||||||
|
const cycle = async (id: string, current: Status) => {
|
||||||
|
const next: Record<Status, Status> = {
|
||||||
|
not_started: "discussed",
|
||||||
|
discussed: "completed",
|
||||||
|
completed: "not_started",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/progress/${encodeURIComponent(id)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: next[current] }),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(await response.text());
|
||||||
|
setProgress((await response.json()).progress);
|
||||||
|
setProgressError("");
|
||||||
|
} catch (error) {
|
||||||
|
setProgressError(
|
||||||
|
`Nie zapisano postępu: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let figureCount = 0;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Topbar toc={model.toc} scale={scale} setScale={setScale} />
|
||||||
|
<main className="paper">
|
||||||
|
<FrontPage front={model.front} />
|
||||||
|
{model.sections.map((section) => {
|
||||||
|
const offset = figureCount;
|
||||||
|
figureCount += section.assets.length;
|
||||||
|
return (
|
||||||
|
<SectionPage
|
||||||
|
key={section.id}
|
||||||
|
section={section}
|
||||||
|
progress={progress}
|
||||||
|
onCycle={cycle}
|
||||||
|
figureOffset={offset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Sheet
|
||||||
|
header={model.dictionary.header_html}
|
||||||
|
className="dictionary-sheet"
|
||||||
|
>
|
||||||
|
<h2>Słownik WE/EN/EK/KW</h2>
|
||||||
|
<Html html={model.dictionary.content_html} />
|
||||||
|
</Sheet>
|
||||||
|
</main>
|
||||||
|
<ProgressDock progress={progress} error={progressError} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const root = createRoot(document.getElementById("root")!);
|
||||||
|
try {
|
||||||
|
const response = await fetch("./card-data.json", { cache: "no-store" });
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(`Nie można wczytać card-data.json: ${response.status}`);
|
||||||
|
const model = (await response.json()) as CardModel;
|
||||||
|
root.render(<App model={model} />);
|
||||||
|
} catch (error) {
|
||||||
|
root.render(
|
||||||
|
<main className="app-load-error">
|
||||||
|
<h1>Nie udało się uruchomić karty</h1>
|
||||||
|
<pre>{error instanceof Error ? error.message : String(error)}</pre>
|
||||||
|
</main>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start();
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
.memory-react {
|
||||||
|
border: 1px solid #555;
|
||||||
|
background: #fff;
|
||||||
|
font:
|
||||||
|
14px/1.35 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
padding: 10px;
|
||||||
|
color: #1b1b1b;
|
||||||
|
}
|
||||||
|
.memory-react > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
}
|
||||||
|
.memory-react > header small {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: #555;
|
||||||
|
font:
|
||||||
|
700 10px/1.2 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
.memory-react h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.memory-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.memory-actions button {
|
||||||
|
border: 1px solid #777;
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.memory-actions button[aria-pressed="true"] {
|
||||||
|
background: #dff3fb;
|
||||||
|
border-color: #237da1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.memory-lanes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(125px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.memory-lane {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid #999;
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 7px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.memory-lane.compile {
|
||||||
|
background: #fff8dc;
|
||||||
|
}
|
||||||
|
.memory-lane.bss {
|
||||||
|
background: #edf7fb;
|
||||||
|
}
|
||||||
|
.memory-lane.stack {
|
||||||
|
background: #f3f8ef;
|
||||||
|
}
|
||||||
|
.memory-lane.registers {
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
.memory-lane.text {
|
||||||
|
background: #f7f0fa;
|
||||||
|
}
|
||||||
|
.memory-lane > b {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.memory-lane button {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
padding: 3px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.memory-lane button:hover,
|
||||||
|
.memory-lane button.active {
|
||||||
|
border-color: #238bb7;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.memory-lane code {
|
||||||
|
font-size: 10px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.arena-wrap {
|
||||||
|
margin: 12px 0 6px;
|
||||||
|
}
|
||||||
|
.arena-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.arena {
|
||||||
|
--allocp: 0;
|
||||||
|
--arena-size: 64;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--arena-size), 1fr);
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid #222;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.arena i {
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.arena .allocp {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--allocp) / var(--arena-size) * 100%);
|
||||||
|
top: 43px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #087da8;
|
||||||
|
font:
|
||||||
|
700 11px/1.2 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.memory-stage {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding-top: 7px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.memory-inspector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 2px 10px;
|
||||||
|
margin-top: 7px;
|
||||||
|
padding: 7px;
|
||||||
|
border-left: 3px solid #238bb7;
|
||||||
|
background: #f5fbfd;
|
||||||
|
}
|
||||||
|
.memory-inspector strong {
|
||||||
|
grid-row: 1/3;
|
||||||
|
}
|
||||||
|
.memory-inspector code {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.memory-inspector span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.asset-static-fallback {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.uml-react {
|
||||||
|
border: 1px solid #555;
|
||||||
|
background: #fff;
|
||||||
|
font:
|
||||||
|
13px/1.35 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
color: #1b1b1b;
|
||||||
|
}
|
||||||
|
.uml-react > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.uml-react > header small {
|
||||||
|
display: block;
|
||||||
|
color: #555;
|
||||||
|
font:
|
||||||
|
700 10px/1.2 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
.uml-react h3 {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.uml-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.uml-actions button {
|
||||||
|
border: 1px solid #777;
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.uml-actions button[aria-pressed="true"] {
|
||||||
|
border-color: #315f78;
|
||||||
|
background: #eaf4fa;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.uml-canvas {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.uml-canvas img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 760px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.uml-stage {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 3px 10px;
|
||||||
|
margin: 0 8px 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-left: 3px solid #315f78;
|
||||||
|
background: #eef7fb;
|
||||||
|
}
|
||||||
|
.uml-stage strong {
|
||||||
|
grid-row: 1/3;
|
||||||
|
}
|
||||||
|
.uml-stage code {
|
||||||
|
grid-column: 2;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.asset-source-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px 0;
|
||||||
|
color: #315f78;
|
||||||
|
font:
|
||||||
|
600 11px/1.3 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
.lesson-progress-control {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 7px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border: 1px solid #8c8c8c;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.lesson-progress-control:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.lesson-progress-control[data-status="discussed"] {
|
||||||
|
background: #fff4cc;
|
||||||
|
border-color: #9b6a00;
|
||||||
|
}
|
||||||
|
.lesson-progress-control[data-status="completed"] {
|
||||||
|
background: #e7f6ed;
|
||||||
|
border-color: #18794e;
|
||||||
|
}
|
||||||
|
.lesson-progress-dock {
|
||||||
|
position: fixed;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
z-index: 30;
|
||||||
|
width: min(320px, calc(100vw - 24px));
|
||||||
|
border: 1px solid #555;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 4px 18px #0003;
|
||||||
|
font:
|
||||||
|
14px/1.35 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
.lesson-progress-dock p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.lesson-progress-error {
|
||||||
|
color: #9b1c1c;
|
||||||
|
}
|
||||||
|
.step-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.section-anchor {
|
||||||
|
position: relative;
|
||||||
|
top: -45px;
|
||||||
|
}
|
||||||
|
.app-load-error {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 10vh auto;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid #9b1c1c;
|
||||||
|
background: #fff;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
.app-load-error pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.memory-lanes {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.memory-react > header,
|
||||||
|
.uml-react > header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.memory-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.memory-react {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.asset-static-fallback {
|
||||||
|
display: block !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.uml-actions,
|
||||||
|
.uml-stage,
|
||||||
|
.asset-source-link,
|
||||||
|
.lesson-progress-dock,
|
||||||
|
.lesson-progress-control {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -260,6 +260,10 @@
|
|||||||
"html_tree_inspector": {
|
"html_tree_inspector": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"react_app": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Generuje komponentowy podgląd React oraz jawny view-model card-data.json."
|
||||||
|
},
|
||||||
"bibliography": {
|
"bibliography": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -575,6 +579,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
|
"html_path": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Opcjonalny wariant zasobu dla HTML, np. SVG zamiast PNG używanego w TeX."
|
||||||
|
},
|
||||||
|
"source_path": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Opcjonalne edytowalne źródło rysunku, np. plik PlantUML."
|
||||||
|
},
|
||||||
"caption": {
|
"caption": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
@@ -596,6 +610,63 @@
|
|||||||
},
|
},
|
||||||
"full_size_link": {
|
"full_size_link": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"interactive": {
|
||||||
|
"$ref": "#/$defs/interactiveAsset"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"interactiveAsset": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["kind"],
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"enum": ["allocator-memory-flow", "uml-sequence"]
|
||||||
|
},
|
||||||
|
"storage_key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"arena_size": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"allocations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symbols": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "area", "declaration", "description"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"area": { "enum": ["compile", "bss", "stack", "registers", "text"] },
|
||||||
|
"declaration": { "type": "string", "minLength": 1 },
|
||||||
|
"description": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "label", "description"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"label": { "type": "string", "minLength": 1 },
|
||||||
|
"description": { "type": "string", "minLength": 1 },
|
||||||
|
"code_ref": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
+351
-30
@@ -699,6 +699,7 @@ def render_preamble(data: dict) -> list[str]:
|
|||||||
r"\usepackage{fancyhdr}",
|
r"\usepackage{fancyhdr}",
|
||||||
r"\usepackage{lastpage}",
|
r"\usepackage{lastpage}",
|
||||||
r"\usepackage{microtype}",
|
r"\usepackage{microtype}",
|
||||||
|
r"\usepackage[most]{tcolorbox}",
|
||||||
r"\usepackage{qrcode}",
|
r"\usepackage{qrcode}",
|
||||||
r"\IfFileExists{references.bib}{%",
|
r"\IfFileExists{references.bib}{%",
|
||||||
r" \usepackage[backend=biber,style=numeric,sorting=none]{biblatex}%",
|
r" \usepackage[backend=biber,style=numeric,sorting=none]{biblatex}%",
|
||||||
@@ -761,6 +762,9 @@ def render_preamble(data: dict) -> list[str]:
|
|||||||
r" \noindent{\color{black!18}\leaders\hbox{\rule{0.60em}{0.22pt}\hspace{0.38em}}\hfill\kern0pt}%",
|
r" \noindent{\color{black!18}\leaders\hbox{\rule{0.60em}{0.22pt}\hspace{0.38em}}\hfill\kern0pt}%",
|
||||||
r" \par\vspace{0.10em}%",
|
r" \par\vspace{0.10em}%",
|
||||||
r"}",
|
r"}",
|
||||||
|
r"\newtcolorbox{ESCTaskFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black,boxsep=0pt,left=1.4mm,right=1.4mm,top=0.8mm,bottom=0.8mm,colbacktitle=black!7,coltitle=black,title={\ttfamily\bfseries TASK\quad #1}}",
|
||||||
|
r"\newtcolorbox{ESCBlockFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!55,boxsep=0pt,left=1.0mm,right=1.0mm,top=0.6mm,bottom=0.6mm,colbacktitle=black!4,coltitle=black,title={\ttfamily\bfseries BLOCK\quad #1}}",
|
||||||
|
r"\newtcolorbox{ESCStepFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!28,boxsep=0pt,left=0.8mm,right=0.8mm,top=0.45mm,bottom=0.45mm,colbacktitle=black!2,coltitle=black,title={\ttfamily STEP\quad #1}}",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -844,6 +848,48 @@ def render_front_matter(card: dict) -> list[str]:
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def render_front_scope_table_tex(scope: dict) -> list[str]:
|
||||||
|
table = scope.get("scope_table") or {}
|
||||||
|
rows = table.get("rows") or []
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
||||||
|
default_headers = (
|
||||||
|
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
||||||
|
if has_task_column
|
||||||
|
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
||||||
|
)
|
||||||
|
headers = table.get("headers") or default_headers
|
||||||
|
if len(headers) != len(default_headers):
|
||||||
|
headers = default_headers
|
||||||
|
column_spec = (
|
||||||
|
r"@{}>{\ttfamily}p{1.05cm}>{\ttfamily}p{1.55cm}X>{\raggedright\arraybackslash}p{3.25cm}@{}"
|
||||||
|
if has_task_column
|
||||||
|
else r"@{}>{\ttfamily}p{1.25cm}X>{\raggedright\arraybackslash}p{3.55cm}@{}"
|
||||||
|
)
|
||||||
|
lines = [
|
||||||
|
r"\vspace{0.55em}",
|
||||||
|
rf"\small\begin{{tabularx}}{{\textwidth}}{{{column_spec}}}",
|
||||||
|
" & ".join(rf"\textbf{{{tex_escape(header)}}}" for header in headers) + r" \\ \hline",
|
||||||
|
]
|
||||||
|
for row in rows:
|
||||||
|
cells = [rf"\texttt{{{tex_escape(row.get('chapter', ''))}}}"]
|
||||||
|
if has_task_column:
|
||||||
|
cells.append(rf"\texttt{{{tex_escape(row.get('task', ''))}}}")
|
||||||
|
cells.extend([row.get("idea_tex", ""), tex_escape(row.get("priority", ""))])
|
||||||
|
if str(row.get("task", "")).strip().lower() == "task04":
|
||||||
|
framed_cells = []
|
||||||
|
for index, cell in enumerate(cells):
|
||||||
|
alignment = "|l|" if index == 0 else "l|"
|
||||||
|
framed_cells.append(rf"\multicolumn{{1}}{{{alignment}}}{{\textbf{{{cell}}}}}")
|
||||||
|
lines.append(r"\hline")
|
||||||
|
lines.append(" & ".join(framed_cells) + r" \\ \hline")
|
||||||
|
else:
|
||||||
|
lines.append(" & ".join(cells) + r" \\ \hline")
|
||||||
|
lines.extend([r"\end{tabularx}", r"\normalsize", ""])
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def render_front_page_scope(data: dict) -> list[str]:
|
def render_front_page_scope(data: dict) -> list[str]:
|
||||||
scope = data.get("front_page_scope")
|
scope = data.get("front_page_scope")
|
||||||
if not scope:
|
if not scope:
|
||||||
@@ -853,18 +899,37 @@ def render_front_page_scope(data: dict) -> list[str]:
|
|||||||
rf"\noindent{{\Large\bfseries {tex_escape(scope.get('title', 'Zakres i cel opracowania'))}}}\par",
|
rf"\noindent{{\Large\bfseries {tex_escape(scope.get('title', 'Zakres i cel opracowania'))}}}\par",
|
||||||
r"\vspace{0.35em}",
|
r"\vspace{0.35em}",
|
||||||
scope.get("content_tex", ""),
|
scope.get("content_tex", ""),
|
||||||
|
]
|
||||||
|
if scope.get("scope_title"):
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
r"\vspace{0.8em}",
|
||||||
|
r"\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}}",
|
||||||
|
r"\vspace{0.7em}",
|
||||||
|
rf"\noindent{{\Large\bfseries {tex_escape(scope['scope_title'])}}}\par",
|
||||||
|
r"\vspace{0.35em}",
|
||||||
|
scope.get("scope_content_tex", ""),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
lines.extend(render_front_scope_table_tex(scope))
|
||||||
|
if scope.get("scope_footer_tex"):
|
||||||
|
lines.extend([scope["scope_footer_tex"], ""])
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
"",
|
"",
|
||||||
r"\vspace{0.8em}",
|
r"\vspace{0.8em}",
|
||||||
r"\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}}",
|
r"\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}}",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def render_task_flow_tex(task_ref: str, task: dict) -> list[str]:
|
def render_task_flow_tex(task_ref: str, task: dict) -> list[str]:
|
||||||
title = task.get("title") or task_ref
|
title = task.get("title") or task_ref
|
||||||
lines = [
|
lines = [
|
||||||
rf"\subsection*{{{tex_escape(task_ref.upper())} · {tex_escape(title)}}}",
|
rf"\begin{{ESCTaskFrame}}{{{tex_escape(task_ref.upper())} · {tex_escape(title)}}}",
|
||||||
]
|
]
|
||||||
if task.get("uuid"):
|
if task.get("uuid"):
|
||||||
lines.append(rf"{{\scriptsize\ttfamily {tex_escape(task['uuid'])}}}\par")
|
lines.append(rf"{{\scriptsize\ttfamily {tex_escape(task['uuid'])}}}\par")
|
||||||
@@ -873,32 +938,33 @@ def render_task_flow_tex(task_ref: str, task: dict) -> list[str]:
|
|||||||
if kind == "block":
|
if kind == "block":
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
r"\par\medskip\noindent\textcolor{black!35}{\rule{\linewidth}{0.35pt}}\par",
|
rf"\begin{{ESCBlockFrame}}{{{tex_escape(item.get('title', ''))}}}",
|
||||||
rf"\noindent{{\large\bfseries Block · {tex_escape(item.get('title', ''))}}}\par",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if item.get("content_tex"):
|
if item.get("content_tex"):
|
||||||
lines.append(str(item["content_tex"]))
|
lines.append(str(item["content_tex"]))
|
||||||
steps = item.get("steps", []) or []
|
steps = item.get("steps", []) or []
|
||||||
if steps:
|
if steps:
|
||||||
lines.append(r"\begin{enumerate}[label=\textbf{K\arabic*.},leftmargin=*]")
|
|
||||||
for step in steps:
|
for step in steps:
|
||||||
content = str(step.get("content_tex", ""))
|
content = str(step.get("content_tex", ""))
|
||||||
suffix = f" {content}" if content else ""
|
lines.append(rf"\begin{{ESCStepFrame}}{{{tex_escape(step.get('id', ''))} · {tex_escape(step.get('title', ''))}}}")
|
||||||
lines.append(rf" \item \textbf{{{tex_escape(step.get('title', ''))}}}{suffix}")
|
if content:
|
||||||
lines.append(r"\end{enumerate}")
|
lines.append(content)
|
||||||
|
lines.append(r"\end{ESCStepFrame}")
|
||||||
|
lines.append(r"\end{ESCBlockFrame}")
|
||||||
elif kind == "exercise":
|
elif kind == "exercise":
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
r"\par\medskip\noindent\textcolor{black!55}{\rule{\linewidth}{0.6pt}}\par",
|
rf"\begin{{ESCBlockFrame}}{{Ćwiczenie · {tex_escape(item.get('title', ''))}}}",
|
||||||
rf"\noindent{{\large\bfseries Exercise · {tex_escape(item.get('title', ''))}}}\par",
|
|
||||||
str(item.get("prompt_tex", "")),
|
str(item.get("prompt_tex", "")),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if item.get("evidence_tex"):
|
if item.get("evidence_tex"):
|
||||||
lines.append(rf"\par\textbf{{Evidence:}} {item['evidence_tex']}")
|
lines.append(rf"\par\textbf{{Evidence:}} {item['evidence_tex']}")
|
||||||
lines.append(rf"\par\textbf{{Acceptance:}} {tex_escape(item.get('criterion', ''))}")
|
lines.append(rf"\par\textbf{{Acceptance:}} {tex_escape(item.get('criterion', ''))}")
|
||||||
lines.append(rf"\par\textbf{{Task acceptance:}} {tex_escape(task.get('criterion', ''))}")
|
lines.append(r"\end{ESCBlockFrame}")
|
||||||
|
lines.append(rf"\tcblower\textbf{{Task acceptance:}} {tex_escape(task.get('criterion', ''))}")
|
||||||
|
lines.append(r"\end{ESCTaskFrame}")
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -906,7 +972,11 @@ def render_tasks(section: dict, data: dict) -> list[str]:
|
|||||||
tasks = data["tasks"]
|
tasks = data["tasks"]
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
legacy_refs: list[str] = []
|
legacy_refs: list[str] = []
|
||||||
for task_ref in section["task_refs"]:
|
for index, task_ref in enumerate(section["task_refs"]):
|
||||||
|
if index:
|
||||||
|
# Kolejny task jest niezależnym etapem karty: jego ramka ma
|
||||||
|
# zaczynać nową stronę, a nie być kontynuacją poprzedniego.
|
||||||
|
lines.append(r"\clearpage")
|
||||||
task = tasks[task_ref]
|
task = tasks[task_ref]
|
||||||
if task.get("flow"):
|
if task.get("flow"):
|
||||||
lines.extend(render_task_flow_tex(task_ref, task))
|
lines.extend(render_task_flow_tex(task_ref, task))
|
||||||
@@ -1747,9 +1817,22 @@ def render_section_margin(section: dict, data: dict) -> list[str]:
|
|||||||
return render_dual_margin_block(general_tags, vocational_tags, voffset="-3.1em", data=data)
|
return render_dual_margin_block(general_tags, vocational_tags, voffset="-3.1em", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def ordered_sections(data: dict) -> list[dict]:
|
||||||
|
return [
|
||||||
|
section
|
||||||
|
for _, section in sorted(
|
||||||
|
enumerate(data.get("sections", [])),
|
||||||
|
key=lambda entry: (entry[1].get("order", 1000), entry[0]),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def render_sections(data: dict) -> list[str]:
|
def render_sections(data: dict) -> list[str]:
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
for section in data["sections"]:
|
for section in ordered_sections(data):
|
||||||
|
if section.get("content_kind") == "tasks":
|
||||||
|
# Pierwszy task danej grupy również dostaje własny początek strony.
|
||||||
|
lines.append(r"\clearpage")
|
||||||
command = r"\section*" if section.get("starred") else r"\section"
|
command = r"\section*" if section.get("starred") else r"\section"
|
||||||
lines.append(r"\ESCSectionBlockStart")
|
lines.append(r"\ESCSectionBlockStart")
|
||||||
lines.append(rf"{command}{{{tex_escape(section['title'])}}}")
|
lines.append(rf"{command}{{{tex_escape(section['title'])}}}")
|
||||||
@@ -2206,7 +2289,7 @@ def collect_numbered_labels(data: dict) -> tuple[dict[str, int], dict[str, int]]
|
|||||||
figure_numbers: dict[str, int] = {}
|
figure_numbers: dict[str, int] = {}
|
||||||
equation_no = 0
|
equation_no = 0
|
||||||
figure_no = 0
|
figure_no = 0
|
||||||
for section in data.get("sections", []):
|
for section in ordered_sections(data):
|
||||||
content = section.get("content_tex", "")
|
content = section.get("content_tex", "")
|
||||||
for match in re.finditer(
|
for match in re.finditer(
|
||||||
r"\\begin\{(?P<kind>equation|figure)\}.*?\\label\{(?P<label>[^}]+)\}.*?\\end\{(?P=kind)\}",
|
r"\\begin\{(?P<kind>equation|figure)\}.*?\\label\{(?P<label>[^}]+)\}.*?\\end\{(?P=kind)\}",
|
||||||
@@ -2785,6 +2868,7 @@ def render_bibliography_section_html(data: dict) -> str:
|
|||||||
|
|
||||||
def render_main_html(data: dict) -> str:
|
def render_main_html(data: dict) -> str:
|
||||||
equation_numbers, figure_numbers = collect_numbered_labels(data)
|
equation_numbers, figure_numbers = collect_numbered_labels(data)
|
||||||
|
sections = ordered_sections(data)
|
||||||
card = data["card"]
|
card = data["card"]
|
||||||
use_resource_header = bool(data.get("title_block"))
|
use_resource_header = bool(data.get("title_block"))
|
||||||
show_tree_inspector = data.get("generated", {}).get("html_tree_inspector", True)
|
show_tree_inspector = data.get("generated", {}).get("html_tree_inspector", True)
|
||||||
@@ -2795,10 +2879,10 @@ def render_main_html(data: dict) -> str:
|
|||||||
)
|
)
|
||||||
toc = "".join(
|
toc = "".join(
|
||||||
f'<a href="#{html_escape(html_id("section-" + str(index)))}">{index}. {html_escape(section.get("title", ""))}</a>'
|
f'<a href="#{html_escape(html_id("section-" + str(index)))}">{index}. {html_escape(section.get("title", ""))}</a>'
|
||||||
for index, section in enumerate(data.get("sections", []), start=1)
|
for index, section in enumerate(sections, start=1)
|
||||||
)
|
)
|
||||||
bibliography_html = render_bibliography_section_html(data)
|
bibliography_html = render_bibliography_section_html(data)
|
||||||
total_pages = 2 + len(data.get("sections", [])) + (1 if bibliography_html else 0)
|
total_pages = 2 + len(sections) + (1 if bibliography_html else 0)
|
||||||
rows = [
|
rows = [
|
||||||
("Projekt", card["project"]),
|
("Projekt", card["project"]),
|
||||||
("Seria", card.get("series_title", card["series"])),
|
("Seria", card.get("series_title", card["series"])),
|
||||||
@@ -2818,7 +2902,7 @@ def render_main_html(data: dict) -> str:
|
|||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
sections_html: list[str] = []
|
sections_html: list[str] = []
|
||||||
for index, section in enumerate(data.get("sections", []), start=1):
|
for index, section in enumerate(sections, start=1):
|
||||||
page_number = index + 1
|
page_number = index + 1
|
||||||
content = (
|
content = (
|
||||||
render_tasks_html(section, data, equation_numbers, figure_numbers)
|
render_tasks_html(section, data, equation_numbers, figure_numbers)
|
||||||
@@ -2854,7 +2938,7 @@ def render_main_html(data: dict) -> str:
|
|||||||
f'</div></article>'
|
f'</div></article>'
|
||||||
)
|
)
|
||||||
if bibliography_html:
|
if bibliography_html:
|
||||||
bibliography_page = len(data.get("sections", [])) + 2
|
bibliography_page = len(sections) + 2
|
||||||
sections_html.append(
|
sections_html.append(
|
||||||
f'<article class="sheet section-sheet" id="bibliografia-i-zrodla">'
|
f'<article class="sheet section-sheet" id="bibliografia-i-zrodla">'
|
||||||
f'<div class="sheet-content">'
|
f'<div class="sheet-content">'
|
||||||
@@ -2868,11 +2952,45 @@ def render_main_html(data: dict) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
scope = data.get("front_page_scope", {})
|
scope = data.get("front_page_scope", {})
|
||||||
|
scope_html = ""
|
||||||
|
if scope:
|
||||||
scope_html = (
|
scope_html = (
|
||||||
f'<section class="front-scope"><h2>{html_escape(scope.get("title", "Zakres i cel opracowania"))}</h2>'
|
f'<section class="front-scope front-goal"><h2>{html_escape(scope.get("title", "Cel karty"))}</h2>'
|
||||||
f'{render_latex_fragment_html(scope.get("content_tex", ""), equation_numbers, figure_numbers)}</section>'
|
f'{render_latex_fragment_html(scope.get("content_tex", ""), equation_numbers, figure_numbers)}</section>'
|
||||||
if scope
|
)
|
||||||
else ""
|
if scope.get("scope_title"):
|
||||||
|
table = scope.get("scope_table") or {}
|
||||||
|
rows = table.get("rows") or []
|
||||||
|
has_task_column = any(str(row.get("task", "")).strip() for row in rows)
|
||||||
|
default_headers = (
|
||||||
|
["K&R", "Numer tasku", "Najważniejsza idea", "Priorytet"]
|
||||||
|
if has_task_column
|
||||||
|
else ["K&R", "Najważniejsza idea", "Priorytet"]
|
||||||
|
)
|
||||||
|
headers = table.get("headers") or default_headers
|
||||||
|
if len(headers) != len(default_headers):
|
||||||
|
headers = default_headers
|
||||||
|
scope_table_html = ""
|
||||||
|
if rows:
|
||||||
|
table_rows = "".join(
|
||||||
|
f'<tr{" class=\"scope-row--key\"" if str(row.get("task", "")).strip().lower() == "task04" else ""}>'
|
||||||
|
f'<td><code>{html_escape(row.get("chapter", ""))}</code></td>'
|
||||||
|
+ (f'<td><code>{html_escape(row.get("task", ""))}</code></td>' if has_task_column else "")
|
||||||
|
+ f'<td>{latex_inline_to_html(row.get("idea_tex", ""), equation_numbers, figure_numbers)}</td>'
|
||||||
|
+ f'<td>{html_escape(row.get("priority", ""))}</td></tr>'
|
||||||
|
for row in rows
|
||||||
|
)
|
||||||
|
table_class = "scope-table scope-table--task" if has_task_column else "scope-table"
|
||||||
|
scope_table_html = (
|
||||||
|
f'<div class="scope-table-wrap"><table class="{table_class}"><thead><tr>'
|
||||||
|
+ "".join(f'<th>{html_escape(header)}</th>' for header in headers)
|
||||||
|
+ f'</tr></thead><tbody>{table_rows}</tbody></table></div>'
|
||||||
|
)
|
||||||
|
scope_html += (
|
||||||
|
f'<section class="front-scope front-card-scope"><h2>{html_escape(scope["scope_title"])}</h2>'
|
||||||
|
f'{render_latex_fragment_html(scope.get("scope_content_tex", ""), equation_numbers, figure_numbers)}'
|
||||||
|
f'{scope_table_html}'
|
||||||
|
f'{render_latex_fragment_html(scope.get("scope_footer_tex", ""), equation_numbers, figure_numbers)}</section>'
|
||||||
)
|
)
|
||||||
front_intro_html = (
|
front_intro_html = (
|
||||||
scope_html
|
scope_html
|
||||||
@@ -2888,14 +3006,15 @@ def render_main_html(data: dict) -> str:
|
|||||||
css = r"""
|
css = r"""
|
||||||
:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000;--viewer-canvas-scale:1;--viewer-content-scale:1}
|
:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000;--viewer-canvas-scale:1;--viewer-content-scale:1}
|
||||||
*{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:"Latin Modern Roman","LM Roman 12",Georgia,"Times New Roman",serif;line-height:1.25}
|
*{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:"Latin Modern Roman","LM Roman 12",Georgia,"Times New Roman",serif;line-height:1.25}
|
||||||
.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:3px 10px;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:3px 0;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
|
||||||
.topbar-inner{max-width:1120px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:12px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:12px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:11px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 10px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px}
|
.topbar-inner{width:calc(100% - 8px);max-width:none;margin:0 4px;display:flex;align-items:center;justify-content:space-between;gap:6px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:16px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:16px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 16px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px}
|
||||||
.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;height:297mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:hidden;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm}
|
.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:visible;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm}
|
||||||
@media(max-width:900px){.sheet{height:auto}}@media print{.sheet{height:auto}}
|
@media(max-width:900px){.sheet{height:auto}}@media print{.sheet{height:auto}}
|
||||||
.pdf-main{min-width:0;width:100%}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap}
|
.pdf-main{min-width:0;width:100%}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap}
|
||||||
.hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px}
|
.hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px}
|
||||||
.meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}
|
.meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||||
.front-scope{padding:0 0 3mm;border-bottom:1px solid var(--line-soft)}.front-scope h2{font-size:24px;line-height:1.15;margin:0 0 3mm}.front-scope p{font-size:16px}
|
.front-scope{padding:0 0 3mm;border-bottom:1px solid var(--line-soft)}.front-scope h2{font-size:24px;line-height:1.15;margin:0 0 3mm}.front-scope p{font-size:16px}
|
||||||
|
.scope-table-wrap{margin:3mm 0;overflow:hidden}.scope-table{width:100%;border-collapse:collapse;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.22}.scope-table th,.scope-table td{border:0;border-bottom:1px solid #888;padding:4px 7px;text-align:left;vertical-align:top}.scope-table th{border-bottom:2px solid #555;background:#f3f3f3;font-weight:700}.scope-table th:first-child,.scope-table td:first-child{width:12%}.scope-table th:last-child,.scope-table td:last-child{width:34%}.scope-table--task th:first-child,.scope-table--task td:first-child{width:9%}.scope-table--task th:nth-child(2),.scope-table--task td:nth-child(2){width:14%}.scope-table--task th:last-child,.scope-table--task td:last-child{width:29%}.scope-table tr.scope-row--key td{font-weight:700;background:#f4f4f4;border-top:2px solid #111;border-bottom:2px solid #111}.scope-table tr.scope-row--key td:first-child{border-left:2px solid #111}.scope-table tr.scope-row--key td:last-child{border-right:2px solid #111}.scope-table td p{font:inherit;margin:0}.scope-table code{font-size:inherit}
|
||||||
.card-section h2{font-size:24px;line-height:1.15;margin:0 0 4mm}.section-heading{display:flex;align-items:flex-start;gap:0}.section-heading .section-index{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;margin-right:12mm;font-weight:400}.section-heading .section-title{min-width:0}.task-identity{margin-left:auto;padding-left:22px;text-align:right;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;line-height:1.25;color:#555;white-space:nowrap}.task-identity strong,.task-identity-name,.task-identity code{display:block}.task-identity strong{font-size:12px;color:#111}.task-identity-name{max-width:240px;overflow:hidden;text-overflow:ellipsis}.task-identity code{font-size:9px;color:#777}
|
.card-section h2{font-size:24px;line-height:1.15;margin:0 0 4mm}.section-heading{display:flex;align-items:flex-start;gap:0}.section-heading .section-index{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;margin-right:12mm;font-weight:400}.section-heading .section-title{min-width:0}.task-identity{margin-left:auto;padding-left:22px;text-align:right;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;line-height:1.25;color:#555;white-space:nowrap}.task-identity strong,.task-identity-name,.task-identity code{display:block}.task-identity strong{font-size:12px;color:#111}.task-identity-name{max-width:240px;overflow:hidden;text-overflow:ellipsis}.task-identity code{font-size:9px;color:#777}
|
||||||
p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm;padding:0}.pdf-main li{margin:1.2mm 0}
|
p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm;padding:0}.pdf-main li{margin:1.2mm 0}
|
||||||
.pdf-margin{position:absolute;top:34mm;width:18mm;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:5px;line-height:1.12;color:#333;max-height:245mm;overflow:hidden}.pdf-margin.left{left:2mm;text-align:left}.pdf-margin.right{right:2mm;text-align:left}
|
.pdf-margin{position:absolute;top:34mm;width:18mm;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:5px;line-height:1.12;color:#333;max-height:245mm;overflow:hidden}.pdf-margin.left{left:2mm;text-align:left}.pdf-margin.right{right:2mm;text-align:left}
|
||||||
@@ -2907,10 +3026,10 @@ p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm
|
|||||||
figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in}
|
figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in}
|
||||||
.table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white}
|
.table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white}
|
||||||
.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em}
|
.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em}
|
||||||
code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #aaa;margin:2mm 0 4mm}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:10px;padding:8px 11px;background:#f3f3f3;border-bottom:1px solid #aaa}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#777}.task-block,.task-exercise{margin:10px 12px;padding:0 0 10px;border-bottom:1px solid #ddd}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#666}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{counter-reset:block-step;margin:5px 0 0 24px!important}.block-steps li{margin:7px 0}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace}.task-exercise{border:1px solid #777;border-left:4px solid #111;padding:9px 11px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:8px;padding-top:6px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:7px 0 0}.task-flow>footer{padding:8px 11px;border-top:1px solid #aaa;font-size:12px}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none}
|
code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #222;margin:1mm 0 2mm;background:#fff}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:6px;padding:5px 7px;background:#eaeaea;border-bottom:1px solid #222}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#222}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#555}.task-block{margin:6px 7px;padding:5px 6px;border:1px solid #777;background:#fff}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:5px;margin-bottom:3px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#555}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{margin:4px 0 0 16px!important;padding:0;display:grid;gap:3px}.block-steps li{margin:0;border:1px solid #b9b9b9;padding:4px 5px;background:#fff}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace;font-weight:700}.block-steps li p:last-child{margin-bottom:0}.task-exercise{margin:6px 7px;border:1px solid #777;border-left:1px solid #222;padding:5px 6px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:5px;padding-top:4px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:4px 0 0}.task-flow>footer{padding:5px 7px;border-top:1px solid #222;font-size:12px;background:#f6f6f6}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none}
|
||||||
.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)}
|
.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)}
|
||||||
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}.viewer-zoom-status{font-size:10px}}
|
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}.viewer-zoom-status{font-size:16px}}
|
||||||
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,figure{break-inside:avoid}}
|
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,.block-steps li,figure{break-inside:avoid}}
|
||||||
"""
|
"""
|
||||||
css += page_resource_header_css(data)
|
css += page_resource_header_css(data)
|
||||||
if use_resource_header:
|
if use_resource_header:
|
||||||
@@ -2957,7 +3076,8 @@ const viewerSheets = Array.from(document.querySelectorAll('.sheet')).map(sheet =
|
|||||||
sheet,
|
sheet,
|
||||||
content: sheet.querySelector('.sheet-content')
|
content: sheet.querySelector('.sheet-content')
|
||||||
})).filter(item => item.content);
|
})).filter(item => item.content);
|
||||||
let viewerCanvasScale = 1;
|
const viewerCanvasDefault = 2.18;
|
||||||
|
let viewerCanvasScale = viewerCanvasDefault;
|
||||||
let viewerContentScale = 1;
|
let viewerContentScale = 1;
|
||||||
function clampViewerScale(value, minimum, maximum){
|
function clampViewerScale(value, minimum, maximum){
|
||||||
return Math.min(maximum, Math.max(minimum, value));
|
return Math.min(maximum, Math.max(minimum, value));
|
||||||
@@ -3029,7 +3149,7 @@ if(viewerPaper){
|
|||||||
}
|
}
|
||||||
if(viewerZoomReset){
|
if(viewerZoomReset){
|
||||||
viewerZoomReset.addEventListener('click', () => {
|
viewerZoomReset.addEventListener('click', () => {
|
||||||
viewerCanvasScale = Math.min(1, viewerCanvasMaximum());
|
viewerCanvasScale = Math.min(viewerCanvasDefault, viewerCanvasMaximum());
|
||||||
viewerContentScale = 1;
|
viewerContentScale = 1;
|
||||||
refreshViewerLayout();
|
refreshViewerLayout();
|
||||||
});
|
});
|
||||||
@@ -6782,7 +6902,8 @@ def prepare_html_figure_assets(data: dict) -> None:
|
|||||||
for graphic in graphics_paths(data.get("sections", [])):
|
for graphic in graphics_paths(data.get("sections", [])):
|
||||||
add_graphic(graphic)
|
add_graphic(graphic)
|
||||||
for asset in section_assets(data.get("sections", [])):
|
for asset in section_assets(data.get("sections", [])):
|
||||||
add_graphic(str(asset.get("path", "")))
|
add_graphic(str(asset.get("html_path") or asset.get("path", "")))
|
||||||
|
add_graphic(str(asset.get("source_path", "")))
|
||||||
for figure in data.get("figures", []):
|
for figure in data.get("figures", []):
|
||||||
add_graphic(figure.get("path", ""))
|
add_graphic(figure.get("path", ""))
|
||||||
pdf_links: list[str] = []
|
pdf_links: list[str] = []
|
||||||
@@ -6820,6 +6941,205 @@ def prepare_html_figure_assets(data: dict) -> None:
|
|||||||
shutil.copy2(source, output_dir / source.name)
|
shutil.copy2(source, output_dir / source.name)
|
||||||
|
|
||||||
|
|
||||||
|
def react_task_model(
|
||||||
|
task_ref: str,
|
||||||
|
task: dict,
|
||||||
|
equation_numbers: dict[str, int],
|
||||||
|
figure_numbers: dict[str, int],
|
||||||
|
) -> dict:
|
||||||
|
flow: list[dict] = []
|
||||||
|
for item in task.get("flow", []) or []:
|
||||||
|
flow.append(
|
||||||
|
{
|
||||||
|
"id": str(item.get("id", "")),
|
||||||
|
"kind": str(item.get("kind", "block")),
|
||||||
|
"title": str(item.get("title", "")),
|
||||||
|
"content_html": render_latex_fragment_html(
|
||||||
|
str(item.get("content_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": str(step.get("id", "")),
|
||||||
|
"title": str(step.get("title", "")),
|
||||||
|
"content_html": render_latex_fragment_html(
|
||||||
|
str(step.get("content_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for step in item.get("steps", []) or []
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"ref": task_ref,
|
||||||
|
"title": str(task.get("title") or task_ref),
|
||||||
|
"uuid": str(task.get("uuid", "")),
|
||||||
|
"criterion": str(task.get("criterion", "")),
|
||||||
|
"prompt_html": render_latex_fragment_html(
|
||||||
|
str(task.get("prompt_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
"flow": flow,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def react_card_model(data: dict) -> dict:
|
||||||
|
equation_numbers, figure_numbers = collect_numbered_labels(data)
|
||||||
|
sections = ordered_sections(data)
|
||||||
|
page_specs: list[tuple[dict, str | None]] = []
|
||||||
|
for section in sections:
|
||||||
|
task_refs = section.get("task_refs", []) or []
|
||||||
|
if section.get("content_kind") == "tasks" and task_refs:
|
||||||
|
page_specs.extend((section, str(task_ref)) for task_ref in task_refs)
|
||||||
|
else:
|
||||||
|
page_specs.append((section, None))
|
||||||
|
total_pages = 2 + len(page_specs)
|
||||||
|
front_left, front_right = render_dual_margin_html(
|
||||||
|
learning_tree_tags(data, column="ogolne", effect_color="green!50!black"),
|
||||||
|
learning_tree_tags(data, column="zawodowe", effect_color="orange!85!black"),
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
scope = data.get("front_page_scope", {}) or {}
|
||||||
|
scope_table = scope.get("scope_table", {}) or {}
|
||||||
|
front = {
|
||||||
|
"header_html": render_page_header_html(data, 1, total_pages),
|
||||||
|
"left_margin_html": front_left,
|
||||||
|
"right_margin_html": front_right,
|
||||||
|
"goal_title": str(scope.get("title", "Cel karty")),
|
||||||
|
"goal_html": render_latex_fragment_html(
|
||||||
|
str(scope.get("content_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
"scope_title": str(scope.get("scope_title", "Zakres karty")),
|
||||||
|
"scope_html": render_latex_fragment_html(
|
||||||
|
str(scope.get("scope_content_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
"scope_headers": list(scope_table.get("headers", [])),
|
||||||
|
"scope_rows": [
|
||||||
|
{
|
||||||
|
"chapter": str(row.get("chapter", "")),
|
||||||
|
"task": str(row.get("task", "")),
|
||||||
|
"idea_html": render_latex_fragment_html(
|
||||||
|
str(row.get("idea_tex", row.get("idea", ""))), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
"priority": str(row.get("priority", "")),
|
||||||
|
"key": bool(row.get("key", str(row.get("task", "")).lower() == "task04")),
|
||||||
|
}
|
||||||
|
for row in scope_table.get("rows", []) or []
|
||||||
|
],
|
||||||
|
}
|
||||||
|
section_models: list[dict] = []
|
||||||
|
for index, (section, task_ref) in enumerate(page_specs, start=1):
|
||||||
|
left_margin, right_margin = render_section_margins_html(section, data)
|
||||||
|
task_refs = section.get("task_refs", []) or []
|
||||||
|
tasks = (
|
||||||
|
[react_task_model(task_ref, data["tasks"][task_ref], equation_numbers, figure_numbers)]
|
||||||
|
if task_ref
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
assets = []
|
||||||
|
include_section_assets = not task_ref or task_ref == str(task_refs[0])
|
||||||
|
for asset in (section.get("assets", []) or []) if include_section_assets else []:
|
||||||
|
source_path = str(asset.get("html_path") or asset.get("path", ""))
|
||||||
|
assets.append(
|
||||||
|
{
|
||||||
|
"label": str(asset.get("label", "")),
|
||||||
|
"caption": str(asset.get("caption", "")),
|
||||||
|
"alt": str(asset.get("alt") or asset.get("caption", "")),
|
||||||
|
"href": html_figure_asset(source_path),
|
||||||
|
"source_href": html_figure_asset(str(asset.get("source_path", "")))
|
||||||
|
if asset.get("source_path")
|
||||||
|
else "",
|
||||||
|
"width": float(asset.get("width", 1.0)),
|
||||||
|
"kind": str(asset.get("kind", "diagram")),
|
||||||
|
"interactive": asset.get("interactive"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
title = str(section.get("title", ""))
|
||||||
|
if task_ref and len(task_refs) > 1:
|
||||||
|
task_title = str(data["tasks"][task_ref].get("title", "")).strip()
|
||||||
|
title = f'{task_ref.upper()}{" — " + task_title if task_title else ""}'
|
||||||
|
section_models.append(
|
||||||
|
{
|
||||||
|
"id": html_id(f'section-{index}-{task_ref or "content"}'),
|
||||||
|
"index": index,
|
||||||
|
"title": title,
|
||||||
|
"header_html": render_page_header_html(data, index + 1, total_pages),
|
||||||
|
"left_margin_html": left_margin,
|
||||||
|
"right_margin_html": right_margin,
|
||||||
|
"content_html": ""
|
||||||
|
if section.get("content_kind") == "tasks"
|
||||||
|
else render_latex_fragment_html(
|
||||||
|
str(section.get("content_tex", "")), equation_numbers, figure_numbers
|
||||||
|
),
|
||||||
|
"tasks": tasks,
|
||||||
|
"assets": assets,
|
||||||
|
"steps": [
|
||||||
|
{"id": str(step.get("id", "")), "title": str(step.get("title", ""))}
|
||||||
|
for step in section.get("steps", []) or []
|
||||||
|
] if (not task_ref or task_ref == str(task_refs[0])) else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"schema": "esc-card-react-view.v1",
|
||||||
|
"card": data.get("card", {}),
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"front": front,
|
||||||
|
"sections": section_models,
|
||||||
|
"dictionary": {
|
||||||
|
"header_html": render_page_header_html(data, total_pages, total_pages),
|
||||||
|
"content_html": render_learning_tree_overview_html(data),
|
||||||
|
},
|
||||||
|
"toc": [
|
||||||
|
{"id": section["id"], "label": f'{section["index"]}. {section["title"]}'}
|
||||||
|
for section in section_models
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_generated_text(content: str) -> str:
|
||||||
|
return "\n".join(line.rstrip() for line in content.splitlines()) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def write_react_app(data: dict, template: dict, legacy_html: str) -> list[Path]:
|
||||||
|
if not data.get("generated", {}).get("react_app"):
|
||||||
|
return []
|
||||||
|
if template_emitter(template, "html") != "classic":
|
||||||
|
raise RuntimeError("React renderer obsługuje obecnie emitter HTML 'classic'")
|
||||||
|
web_root = html_output_dir(data)
|
||||||
|
web_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
style_match = re.search(r"<style>(.*?)</style>", legacy_html, re.S)
|
||||||
|
base_css = style_match.group(1) if style_match else ""
|
||||||
|
model_path = web_root / "card-data.json"
|
||||||
|
style_path = web_root / "style.css"
|
||||||
|
index_path = web_root / "index.html"
|
||||||
|
model_path.write_text(json.dumps(react_card_model(data), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
style_path.write_text(normalize_generated_text(base_css), encoding="utf-8")
|
||||||
|
index_path.write_text(
|
||||||
|
"""<!doctype html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="card-renderer" content="react">
|
||||||
|
<title>Karta pracy</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="app.css">
|
||||||
|
</head>
|
||||||
|
<body class="resource-header-enabled">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
build_script = REPO_ROOT / "react" / "build.mjs"
|
||||||
|
subprocess.run(
|
||||||
|
["node", str(build_script), str(web_root)],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return [index_path, style_path, model_path, web_root / "app.js", web_root / "app.css"]
|
||||||
|
|
||||||
|
|
||||||
def write_outputs(data: dict, template: dict, force_web: bool = False) -> list[Path]:
|
def write_outputs(data: dict, template: dict, force_web: bool = False) -> list[Path]:
|
||||||
prepare_html_figure_assets(data)
|
prepare_html_figure_assets(data)
|
||||||
outputs = {
|
outputs = {
|
||||||
@@ -6840,9 +7160,10 @@ def write_outputs(data: dict, template: dict, force_web: bool = False) -> list[P
|
|||||||
continue
|
continue
|
||||||
path = CARD_ROOT / output_ref
|
path = CARD_ROOT / output_ref
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(content, encoding="utf-8")
|
path.write_text(normalize_generated_text(content), encoding="utf-8")
|
||||||
paths.append(path)
|
paths.append(path)
|
||||||
paths.extend(write_web_outputs(data, template, force_web))
|
paths.extend(write_web_outputs(data, template, force_web))
|
||||||
|
paths.extend(write_react_app(data, template, outputs.get("html", "")))
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user