Compare commits
4 Commits
abacus-rea
...
v4.67.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab490ffea | ||
|
|
8b4dacdc98 | ||
|
|
28fc0a14be | ||
|
|
fffaf1df1d |
@@ -19,18 +19,12 @@ yarn-error.log*
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
**/__tests__
|
||||
**/*.test.ts
|
||||
**/*.test.tsx
|
||||
**/*.spec.ts
|
||||
**/*.spec.tsx
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
.claude
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@@ -52,21 +46,7 @@ packages/core/.venv/
|
||||
|
||||
# Storybook
|
||||
storybook-static
|
||||
**/*.stories.tsx
|
||||
**/*.stories.ts
|
||||
.storybook
|
||||
|
||||
# Deployment files
|
||||
nas-deployment/
|
||||
DEPLOYMENT_PLAN.md
|
||||
|
||||
# SQLite database files (created at runtime)
|
||||
**/data/*.db
|
||||
**/data/*.db-shm
|
||||
**/data/*.db-wal
|
||||
|
||||
# Build artifacts (rebuilt during Docker build)
|
||||
**/dist
|
||||
**/.next
|
||||
**/build
|
||||
**/styled-system
|
||||
DEPLOYMENT_PLAN.md
|
||||
16
.mcp.json
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlite": {
|
||||
"command": "/Users/antialias/.nvm/versions/node/v20.19.3/bin/npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-server-sqlite-npx",
|
||||
"/Users/antialias/projects/soroban-abacus-flashcards/apps/web/data/sqlite.db"
|
||||
],
|
||||
"env": {
|
||||
"PATH": "/Users/antialias/.nvm/versions/node/v20.19.3/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin",
|
||||
"NODE_PATH": "/Users/antialias/.nvm/versions/node/v20.19.3/lib/node_modules"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25978
CHANGELOG.md
78
Dockerfile
@@ -16,7 +16,7 @@ COPY packages/core/client/node/package.json ./packages/core/client/node/
|
||||
COPY packages/abacus-react/package.json ./packages/abacus-react/
|
||||
COPY packages/templates/package.json ./packages/templates/
|
||||
|
||||
# Install ALL dependencies for build stage
|
||||
# Install dependencies (will use .npmrc with hoisted mode)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Builder stage
|
||||
@@ -44,71 +44,16 @@ RUN cd apps/web && npx @pandacss/dev
|
||||
# Build using turbo for apps/web and its dependencies
|
||||
RUN turbo build --filter=@soroban/web
|
||||
|
||||
# Production dependencies stage - install only runtime dependencies
|
||||
# IMPORTANT: Must use same base as runner stage for binary compatibility (better-sqlite3)
|
||||
FROM node:18-slim AS deps
|
||||
WORKDIR /app
|
||||
|
||||
# Install build tools temporarily for better-sqlite3 installation
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm@9.15.4
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY apps/web/package.json ./apps/web/
|
||||
COPY packages/core/client/node/package.json ./packages/core/client/node/
|
||||
COPY packages/abacus-react/package.json ./packages/abacus-react/
|
||||
COPY packages/templates/package.json ./packages/templates/
|
||||
|
||||
# Install ONLY production dependencies
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
|
||||
# Typst builder stage - download and prepare typst binary
|
||||
FROM node:18-slim AS typst-builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
xz-utils \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN ARCH=$(uname -m) && \
|
||||
if [ "$ARCH" = "x86_64" ]; then \
|
||||
TYPST_ARCH="x86_64-unknown-linux-musl"; \
|
||||
elif [ "$ARCH" = "aarch64" ]; then \
|
||||
TYPST_ARCH="aarch64-unknown-linux-musl"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $ARCH" && exit 1; \
|
||||
fi && \
|
||||
TYPST_VERSION="v0.13.0" && \
|
||||
wget -q "https://github.com/typst/typst/releases/download/${TYPST_VERSION}/typst-${TYPST_ARCH}.tar.xz" && \
|
||||
tar -xf "typst-${TYPST_ARCH}.tar.xz" && \
|
||||
mv "typst-${TYPST_ARCH}/typst" /usr/local/bin/typst && \
|
||||
chmod +x /usr/local/bin/typst
|
||||
|
||||
# Production image
|
||||
FROM node:18-slim AS runner
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Install ONLY runtime dependencies (no build tools)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
qpdf \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy typst binary from typst-builder stage
|
||||
COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst
|
||||
# Install Python, pip, build tools for better-sqlite3, Typst, and qpdf (needed at runtime)
|
||||
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst qpdf
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy built Next.js application
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./apps/web/.next
|
||||
@@ -124,9 +69,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/dist ./apps/web/dist
|
||||
# Copy database migrations
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/drizzle ./apps/web/drizzle
|
||||
|
||||
# Copy PRODUCTION node_modules only (no dev dependencies)
|
||||
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=deps --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
|
||||
# Copy node_modules (for dependencies)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
|
||||
|
||||
# Copy core package (needed for Python flashcard generation scripts)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
|
||||
@@ -134,9 +79,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
|
||||
# Copy templates package (needed for Typst templates)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/packages/templates ./packages/templates
|
||||
|
||||
# Copy abacus-react package (needed for calendar generation scripts)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/packages/abacus-react ./packages/abacus-react
|
||||
|
||||
# Install Python dependencies for flashcard generation
|
||||
RUN pip3 install --no-cache-dir --break-system-packages -r packages/core/requirements.txt
|
||||
|
||||
@@ -157,4 +99,4 @@ ENV HOSTNAME "0.0.0.0"
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,325 +0,0 @@
|
||||
# 3D Printing Docker Setup
|
||||
|
||||
## Summary
|
||||
|
||||
The 3D printable abacus customization feature is fully containerized with optimized Docker multi-stage builds.
|
||||
|
||||
**Key Technologies:**
|
||||
- OpenSCAD 2021.01 (for rendering STL/3MF from .scad files)
|
||||
- BOSL2 v2.0.0 (minimized library, .scad files only)
|
||||
- Typst v0.11.1 (pre-built binary)
|
||||
|
||||
**Image Size:** ~257MB (optimized with multi-stage builds, saved ~38MB)
|
||||
|
||||
**Build Stages:** 7 total (base → builder → deps → typst-builder → bosl2-builder → runner)
|
||||
|
||||
## Overview
|
||||
|
||||
The 3D printable abacus customization feature requires OpenSCAD and the BOSL2 library to be available in the Docker container.
|
||||
|
||||
## Size Optimization Strategy
|
||||
|
||||
The Dockerfile uses **multi-stage builds** to minimize the final image size:
|
||||
|
||||
1. **typst-builder stage** - Downloads and extracts typst, discards wget/xz-utils
|
||||
2. **bosl2-builder stage** - Clones BOSL2 and removes unnecessary files (tests, docs, examples, images)
|
||||
3. **runner stage** - Only copies final binaries and minimized libraries
|
||||
|
||||
### Size Reductions
|
||||
|
||||
- **Removed from runner**: git, wget, curl, xz-utils (~40MB)
|
||||
- **BOSL2 minimized**: Removed .git, tests, tutorials, examples, images, markdown files (~2-3MB savings)
|
||||
- **Kept only .scad files** in BOSL2 library
|
||||
|
||||
## Dockerfile Changes
|
||||
|
||||
### Build Stages Overview
|
||||
|
||||
The Dockerfile now has **7 stages**:
|
||||
|
||||
1. **base** (Alpine) - Install build tools and dependencies
|
||||
2. **builder** (Alpine) - Build Next.js application
|
||||
3. **deps** (Alpine) - Install production node_modules
|
||||
4. **typst-builder** (Debian) - Download and extract typst binary
|
||||
5. **bosl2-builder** (Debian) - Clone and minimize BOSL2 library
|
||||
6. **runner** (Debian) - Final production image
|
||||
|
||||
### Stage 1-3: Base, Builder, Deps (unchanged)
|
||||
|
||||
Uses Alpine Linux for building the application (smaller and faster builds).
|
||||
|
||||
### Stage 4: Typst Builder (lines 68-87)
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-slim AS typst-builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
xz-utils \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN ARCH=$(uname -m) && \
|
||||
... download and install typst from GitHub releases
|
||||
```
|
||||
|
||||
**Purpose:** Download typst binary in isolation, then discard build tools (wget, xz-utils).
|
||||
|
||||
**Result:** Only the typst binary is copied to runner stage (line 120).
|
||||
|
||||
### Stage 5: BOSL2 Builder (lines 90-103)
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-slim AS bosl2-builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /bosl2 && \
|
||||
cd /bosl2 && \
|
||||
git clone --depth 1 --branch v2.0.0 https://github.com/BelfrySCAD/BOSL2.git . && \
|
||||
# Remove unnecessary files to minimize size
|
||||
rm -rf .git .github tests tutorials examples images *.md CONTRIBUTING* LICENSE* && \
|
||||
# Keep only .scad files and essential directories
|
||||
find . -type f ! -name "*.scad" -delete && \
|
||||
find . -type d -empty -delete
|
||||
```
|
||||
|
||||
**Purpose:** Clone BOSL2 and aggressively minimize by removing:
|
||||
- `.git` directory
|
||||
- Tests, tutorials, examples
|
||||
- Documentation (markdown files)
|
||||
- Images
|
||||
- All non-.scad files
|
||||
|
||||
**Result:** Minimized BOSL2 library (~1-2MB instead of ~5MB) copied to runner (line 124).
|
||||
|
||||
### Stage 6: Runner - Production Image (lines 106-177)
|
||||
|
||||
**Base Image:** `node:18-slim` (Debian) - Required for OpenSCAD availability
|
||||
|
||||
**Runtime Dependencies (lines 111-117):**
|
||||
|
||||
```dockerfile
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
qpdf \
|
||||
openscad \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
**Removed from runner:**
|
||||
- ❌ git (only needed in bosl2-builder)
|
||||
- ❌ wget (only needed in typst-builder)
|
||||
- ❌ curl (not needed at runtime)
|
||||
- ❌ xz-utils (only needed in typst-builder)
|
||||
|
||||
**Artifacts Copied from Other Stages:**
|
||||
|
||||
```dockerfile
|
||||
# From typst-builder (line 120)
|
||||
COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst
|
||||
|
||||
# From bosl2-builder (line 124)
|
||||
COPY --from=bosl2-builder /bosl2 /usr/share/openscad/libraries/BOSL2
|
||||
|
||||
# From builder (lines 131-159)
|
||||
# Next.js app, styled-system, server files, etc.
|
||||
|
||||
# From deps (lines 145-146)
|
||||
# Production node_modules only
|
||||
```
|
||||
|
||||
BOSL2 v2.0.0 (minimized) is copied to `/usr/share/openscad/libraries/BOSL2/`, which is OpenSCAD's default library search path. This allows `include <BOSL2/std.scad>` to work in the abacus.scad file.
|
||||
|
||||
### Temp Directory for Job Outputs (line 168)
|
||||
|
||||
```dockerfile
|
||||
RUN mkdir -p tmp/3d-jobs && chown nextjs:nodejs tmp
|
||||
```
|
||||
|
||||
Creates the directory where JobManager stores generated 3D files.
|
||||
|
||||
## Files Included in Docker Image
|
||||
|
||||
The following files are automatically included via the `COPY` command at line 132:
|
||||
|
||||
```
|
||||
apps/web/public/3d-models/
|
||||
├── abacus.scad (parametric OpenSCAD source)
|
||||
└── simplified.abacus.stl (base model, 4.8MB)
|
||||
```
|
||||
|
||||
These files are NOT excluded by `.dockerignore`.
|
||||
|
||||
## Testing the Docker Build
|
||||
|
||||
### Local Testing
|
||||
|
||||
1. **Build the Docker image:**
|
||||
```bash
|
||||
docker build -t soroban-abacus-test .
|
||||
```
|
||||
|
||||
2. **Run the container:**
|
||||
```bash
|
||||
docker run -p 3000:3000 soroban-abacus-test
|
||||
```
|
||||
|
||||
3. **Test OpenSCAD inside the container:**
|
||||
```bash
|
||||
docker exec -it <container-id> sh
|
||||
openscad --version
|
||||
ls /usr/share/openscad/libraries/BOSL2
|
||||
```
|
||||
|
||||
4. **Test the 3D printing endpoint:**
|
||||
- Visit http://localhost:3000/3d-print
|
||||
- Adjust parameters and generate a file
|
||||
- Monitor job progress
|
||||
- Download the result
|
||||
|
||||
### Verify BOSL2 Installation
|
||||
|
||||
Inside the running container:
|
||||
|
||||
```bash
|
||||
# Check OpenSCAD version
|
||||
openscad --version
|
||||
|
||||
# Verify BOSL2 library exists
|
||||
ls -la /usr/share/openscad/libraries/BOSL2/
|
||||
|
||||
# Test rendering a simple file
|
||||
cd /app/apps/web/public/3d-models
|
||||
openscad -o /tmp/test.stl abacus.scad
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Environment Variables
|
||||
|
||||
No additional environment variables are required for the 3D printing feature.
|
||||
|
||||
### Volume Mounts (Optional)
|
||||
|
||||
For better performance and to avoid rebuilding the image when updating 3D models:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 \
|
||||
-v $(pwd)/apps/web/public/3d-models:/app/apps/web/public/3d-models:ro \
|
||||
soroban-abacus-test
|
||||
```
|
||||
|
||||
### Disk Space Considerations
|
||||
|
||||
- **BOSL2 library**: ~5MB (cloned during build)
|
||||
- **Base STL file**: 4.8MB (in public/3d-models/)
|
||||
- **Generated files**: Vary by parameters, typically 1-10MB each
|
||||
- **Job cleanup**: Old jobs are automatically cleaned up after 1 hour
|
||||
|
||||
## Image Size
|
||||
|
||||
The final image is Debian-based (required for OpenSCAD), but optimized using multi-stage builds:
|
||||
|
||||
**Before optimization (original Debian approach):**
|
||||
- Base runner: ~250MB
|
||||
- With all build tools (git, wget, curl, xz-utils): ~290MB
|
||||
- With BOSL2 (full): ~295MB
|
||||
- **Total: ~295MB**
|
||||
|
||||
**After optimization (current multi-stage approach):**
|
||||
- Base runner: ~250MB
|
||||
- Runtime deps only (no build tools): ~250MB
|
||||
- BOSL2 (minimized, .scad only): ~252MB
|
||||
- 3D models (STL): ~257MB
|
||||
- **Total: ~257MB**
|
||||
|
||||
**Savings: ~38MB (~13% reduction)**
|
||||
|
||||
### What Was Removed
|
||||
|
||||
- ❌ git (~15MB)
|
||||
- ❌ wget (~2MB)
|
||||
- ❌ curl (~5MB)
|
||||
- ❌ xz-utils (~1MB)
|
||||
- ❌ BOSL2 .git directory (~1MB)
|
||||
- ❌ BOSL2 tests, examples, tutorials (~10MB)
|
||||
- ❌ BOSL2 images and docs (~4MB)
|
||||
|
||||
**Total removed: ~38MB**
|
||||
|
||||
This trade-off (Debian vs Alpine) is necessary for OpenSCAD availability, but the multi-stage approach minimizes the size impact.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OpenSCAD Not Found
|
||||
|
||||
If you see "openscad: command not found" in logs:
|
||||
|
||||
1. Verify OpenSCAD is installed:
|
||||
```bash
|
||||
docker exec -it <container-id> which openscad
|
||||
docker exec -it <container-id> openscad --version
|
||||
```
|
||||
|
||||
2. Check if the Debian package install succeeded:
|
||||
```bash
|
||||
docker exec -it <container-id> dpkg -l | grep openscad
|
||||
```
|
||||
|
||||
### BOSL2 Include Error
|
||||
|
||||
If OpenSCAD reports "Can't open library 'BOSL2/std.scad'":
|
||||
|
||||
1. Check BOSL2 exists:
|
||||
```bash
|
||||
docker exec -it <container-id> ls /usr/share/openscad/libraries/BOSL2/std.scad
|
||||
```
|
||||
|
||||
2. Test include path:
|
||||
```bash
|
||||
docker exec -it <container-id> sh -c "cd /tmp && echo 'include <BOSL2/std.scad>; cube(10);' > test.scad && openscad -o test.stl test.scad"
|
||||
```
|
||||
|
||||
### Job Fails with "Permission Denied"
|
||||
|
||||
Check tmp directory permissions:
|
||||
|
||||
```bash
|
||||
docker exec -it <container-id> ls -la /app/apps/web/tmp
|
||||
# Should show: drwxr-xr-x ... nextjs nodejs ... 3d-jobs
|
||||
```
|
||||
|
||||
### Large File Generation Timeout
|
||||
|
||||
Jobs timeout after 60 seconds. For complex models, increase the timeout in `jobManager.ts:138`:
|
||||
|
||||
```typescript
|
||||
timeout: 120000, // 2 minutes instead of 60 seconds
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Cold start**: First generation takes ~5-10 seconds (OpenSCAD initialization)
|
||||
- **Warm generations**: Subsequent generations take ~3-5 seconds
|
||||
- **STL size**: Typically 5-15MB depending on scale parameters
|
||||
- **3MF size**: Similar to STL (no significant compression)
|
||||
- **SCAD size**: ~1KB (just text parameters)
|
||||
|
||||
## Monitoring
|
||||
|
||||
Job processing is logged to stdout:
|
||||
|
||||
```
|
||||
Executing: openscad -o /app/apps/web/tmp/3d-jobs/abacus-abc123.stl ...
|
||||
Job abc123 completed successfully
|
||||
```
|
||||
|
||||
Check logs with:
|
||||
|
||||
```bash
|
||||
docker logs <container-id> | grep "Job"
|
||||
```
|
||||
@@ -1,420 +0,0 @@
|
||||
# Card Sorting: Multiplayer & Spectator Features Plan
|
||||
|
||||
## Overview
|
||||
Add collaborative and competitive multiplayer modes to the card-sorting game, plus enhanced spectator experience with real-time player indicators.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Feature: Player Emoji on Moving Cards
|
||||
|
||||
**When any player (including network players) moves a card, show their emoji on it.**
|
||||
|
||||
### Data Structure Changes
|
||||
|
||||
#### `CardPosition` type enhancement:
|
||||
```typescript
|
||||
export interface CardPosition {
|
||||
cardId: string
|
||||
x: number // % of viewport width (0-100)
|
||||
y: number // % of viewport height (0-100)
|
||||
rotation: number // degrees (-15 to 15)
|
||||
zIndex: number
|
||||
draggedByPlayerId?: string // NEW: ID of player currently dragging this card
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **When starting drag (local player):**
|
||||
- Set `draggedByPlayerId` to current player's ID
|
||||
- Broadcast position update immediately with this field
|
||||
|
||||
2. **During drag:**
|
||||
- Continue including `draggedByPlayerId` in position updates
|
||||
- Other clients show the emoji overlay
|
||||
|
||||
3. **When ending drag:**
|
||||
- Clear `draggedByPlayerId` (set to `undefined`)
|
||||
- Broadcast final position without this field
|
||||
|
||||
4. **Visual indicator:**
|
||||
- Show player emoji in top-right corner of card
|
||||
- Semi-transparent background circle
|
||||
- Small size (24-28px diameter)
|
||||
- Positioned absolutely within card container
|
||||
- Example styling:
|
||||
```typescript
|
||||
{
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
right: '4px',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '18px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
border: '2px solid rgba(59, 130, 246, 0.6)',
|
||||
zIndex: 10
|
||||
}
|
||||
```
|
||||
|
||||
5. **Access to player metadata:**
|
||||
- Need to map `playerId` → `PlayerMetadata`
|
||||
- Current state only has single `playerMetadata`
|
||||
- For multiplayer, Provider needs to maintain `players: Map<string, PlayerMetadata>`
|
||||
- Get from room members data
|
||||
|
||||
---
|
||||
|
||||
## 2. Spectator Mode UI Enhancements
|
||||
|
||||
### 2.1 Spectator Banner
|
||||
**Top banner that clearly indicates spectator status**
|
||||
|
||||
```typescript
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '48px',
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 24px',
|
||||
zIndex: 100,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
|
||||
}}>
|
||||
<div>👀 Spectating: {playerName} {playerEmoji}</div>
|
||||
<div>Progress: {cardsPlaced}/{totalCards} cards placed</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2.2 Educational Mode Toggle
|
||||
**Allow spectators to see the correct answer (for learning)**
|
||||
|
||||
- Toggle button in spectator banner
|
||||
- When enabled: show faint green checkmarks on correctly positioned cards
|
||||
- Don't show actual numbers unless player revealed them
|
||||
|
||||
### 2.3 Player Stats Sidebar
|
||||
**Show real-time stats (optional, can collapse)**
|
||||
|
||||
- Time elapsed
|
||||
- Cards placed vs. total
|
||||
- Number of moves made
|
||||
- Current accuracy (% of cards in correct relative order)
|
||||
|
||||
---
|
||||
|
||||
## 3. Collaborative Mode: "Team Sort"
|
||||
|
||||
### 3.1 Core Mechanics
|
||||
- Multiple players share the same board and card set
|
||||
- Anyone can move any card at any time
|
||||
- Shared timer and shared score
|
||||
- Team wins/loses together
|
||||
|
||||
### 3.2 State Changes
|
||||
|
||||
#### `CardSortingState` additions:
|
||||
```typescript
|
||||
export interface CardSortingState extends GameState {
|
||||
// ... existing fields ...
|
||||
|
||||
gameMode: 'solo' | 'collaborative' | 'competitive' | 'relay' // NEW
|
||||
players: Map<string, PlayerMetadata> // NEW: all active players
|
||||
activePlayers: string[] // NEW: players currently in game (not spectators)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Visual Indicators
|
||||
|
||||
1. **Colored cursors for each player:**
|
||||
- Show a small colored dot/cursor at other players' pointer positions
|
||||
- Color derived from player's emoji or assigned color
|
||||
- Update positions via WebSocket (throttled to 30Hz)
|
||||
|
||||
2. **Card claiming indicator:**
|
||||
- When player starts dragging, show their emoji on card (as per feature #1)
|
||||
- Other players see animated emoji bouncing slightly
|
||||
- Prevents confusion about who's moving what
|
||||
|
||||
3. **Activity feed (optional):**
|
||||
- Small toast notifications for key actions
|
||||
- "🎭 Bob placed card #3"
|
||||
- "🦊 Alice revealed numbers"
|
||||
- Auto-dismiss after 3 seconds
|
||||
|
||||
### 3.4 New Moves
|
||||
|
||||
```typescript
|
||||
// In CardSortingMove union:
|
||||
| {
|
||||
type: 'JOIN_COLLABORATIVE_GAME'
|
||||
playerId: string
|
||||
userId: string
|
||||
timestamp: number
|
||||
data: {
|
||||
playerMetadata: PlayerMetadata
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'LEAVE_COLLABORATIVE_GAME'
|
||||
playerId: string
|
||||
userId: string
|
||||
timestamp: number
|
||||
data: Record<string, never>
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Scoring
|
||||
- Same scoring algorithm but labeled as "Team Score"
|
||||
- All players see the same results
|
||||
- Leaderboard entry records all participants
|
||||
|
||||
---
|
||||
|
||||
## 4. Competitive Mode: "Race Sort"
|
||||
|
||||
### 4.1 Core Mechanics
|
||||
- 2-4 players get the **same** card set
|
||||
- Each player has their **own separate board**
|
||||
- Race to finish first OR best score after time limit
|
||||
- Live leaderboard shows current standings
|
||||
|
||||
### 4.2 State Architecture
|
||||
|
||||
**Problem:** Current state is single-player only.
|
||||
|
||||
**Solution:** Each player needs their own game state, but they're in the same room.
|
||||
|
||||
#### Option A: Separate Sessions
|
||||
- Each competitive player creates their own session
|
||||
- Room tracks all session IDs
|
||||
- Client fetches all sessions and displays them
|
||||
- **Pros:** Minimal changes to game logic
|
||||
- **Cons:** Complex room management
|
||||
|
||||
#### Option B: Multi-Player State (RECOMMENDED)
|
||||
```typescript
|
||||
export interface CompetitiveGameState extends GameState {
|
||||
gameMode: 'competitive'
|
||||
sharedCards: SortingCard[] // Same cards for everyone
|
||||
correctOrder: SortingCard[] // Shared answer
|
||||
playerBoards: Map<string, PlayerBoard> // Each player's board state
|
||||
gameStartTime: number
|
||||
gameEndTime: number | null
|
||||
winners: string[] // Player IDs who completed, in order
|
||||
}
|
||||
|
||||
export interface PlayerBoard {
|
||||
playerId: string
|
||||
placedCards: (SortingCard | null)[]
|
||||
cardPositions: CardPosition[]
|
||||
availableCards: SortingCard[]
|
||||
numbersRevealed: boolean
|
||||
completedAt: number | null
|
||||
scoreBreakdown: ScoreBreakdown | null
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 UI Layout
|
||||
|
||||
**Split-screen view:**
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Leaderboard (top bar) │
|
||||
├──────────────┬──────────────────────┤
|
||||
│ │ │
|
||||
│ Your Board │ Opponent Preview │
|
||||
│ (full size) │ (smaller, ghosted) │
|
||||
│ │ │
|
||||
└──────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
**Your board:**
|
||||
- Normal interactive gameplay
|
||||
- Full size, left side
|
||||
|
||||
**Opponent preview(s):**
|
||||
- Right side (or bottom on mobile)
|
||||
- Smaller scale (50-70% size)
|
||||
- Semi-transparent cards
|
||||
- Shows their real-time positions
|
||||
- Can toggle between different opponents
|
||||
|
||||
**Leaderboard bar:**
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 🥇 Alice (5/8) • 🥈 You (4/8) • ... │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 Spectator View for Competitive
|
||||
- Can watch all players simultaneously
|
||||
- Grid layout showing all boards
|
||||
- Highlight current leader with gold border
|
||||
|
||||
---
|
||||
|
||||
## 5. Hybrid Mode: "Relay Sort" (Future)
|
||||
|
||||
### 5.1 Core Mechanics
|
||||
- Players take turns (30-60 seconds each)
|
||||
- Cumulative team score
|
||||
- Can "pass" turn early
|
||||
- Strategy: communicate via chat about optimal moves
|
||||
|
||||
### 5.2 Turn Management
|
||||
```typescript
|
||||
export interface RelayGameState extends GameState {
|
||||
gameMode: 'relay'
|
||||
turnOrder: string[] // Player IDs
|
||||
currentTurnIndex: number
|
||||
turnStartTime: number
|
||||
turnDuration: number // seconds
|
||||
// ... rest similar to collaborative
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (Do First) ✅
|
||||
- [x] Add `draggedByPlayerId` to `CardPosition`
|
||||
- [x] Show player emoji on cards being dragged
|
||||
- [x] Add `players` map to Provider context
|
||||
- [x] Fetch room members and map to player metadata
|
||||
|
||||
### Phase 2: Spectator Enhancements
|
||||
- [ ] Spectator banner component
|
||||
- [ ] Educational mode toggle
|
||||
- [ ] Stats sidebar (collapsible)
|
||||
|
||||
### Phase 3: Collaborative Mode
|
||||
- [ ] Add `gameMode` to state and config
|
||||
- [ ] Implement JOIN/LEAVE moves
|
||||
- [ ] Colored cursor tracking
|
||||
- [ ] Activity feed notifications
|
||||
- [ ] Team scoring UI
|
||||
|
||||
### Phase 4: Competitive Mode
|
||||
- [ ] Design multi-player state structure
|
||||
- [ ] Refactor Provider for per-player boards
|
||||
- [ ] Split-screen UI layout
|
||||
- [ ] Live leaderboard
|
||||
- [ ] Ghost opponent preview
|
||||
- [ ] Winner determination
|
||||
|
||||
### Phase 5: Polish & Testing
|
||||
- [ ] Mobile responsive layouts
|
||||
- [ ] Performance optimization (many simultaneous players)
|
||||
- [ ] Network resilience (handle disconnects)
|
||||
- [ ] Accessibility (keyboard nav, screen readers)
|
||||
|
||||
---
|
||||
|
||||
## 7. Technical Considerations
|
||||
|
||||
### 7.1 WebSocket Message Frequency
|
||||
- **Current:** Position updates throttled to 100ms (10Hz)
|
||||
- **Collaborative:** May need higher frequency for smoothness
|
||||
- **Recommendation:** 50ms (20Hz) for active drag, 100ms otherwise
|
||||
|
||||
### 7.2 State Synchronization
|
||||
- Use optimistic updates for local player
|
||||
- Reconcile with server state on conflicts
|
||||
- Use timestamp-based conflict resolution
|
||||
|
||||
### 7.3 Player Disconnection Handling
|
||||
- Collaborative: Keep their last positions, mark as "disconnected"
|
||||
- Competitive: Pause their timer, allow rejoin within 60s
|
||||
- Spectators: Just remove from viewer list
|
||||
|
||||
### 7.4 Security & Validation
|
||||
- Server validates all moves (already done)
|
||||
- Prevent players from seeing others' moves before they happen
|
||||
- Rate limit position updates per player
|
||||
|
||||
---
|
||||
|
||||
## 8. Database Schema Changes
|
||||
|
||||
### New Tables
|
||||
|
||||
#### `competitive_rounds` (for competitive mode)
|
||||
```sql
|
||||
CREATE TABLE competitive_rounds (
|
||||
id UUID PRIMARY KEY,
|
||||
room_id UUID REFERENCES arcade_rooms(id),
|
||||
started_at TIMESTAMP,
|
||||
ended_at TIMESTAMP,
|
||||
card_set JSON, -- The shared cards
|
||||
winners JSON -- Array of player IDs in finish order
|
||||
);
|
||||
```
|
||||
|
||||
#### `player_round_results` (for competitive mode)
|
||||
```sql
|
||||
CREATE TABLE player_round_results (
|
||||
id UUID PRIMARY KEY,
|
||||
round_id UUID REFERENCES competitive_rounds(id),
|
||||
player_id UUID,
|
||||
score_breakdown JSON,
|
||||
completed_at TIMESTAMP,
|
||||
final_placement INTEGER -- 1st, 2nd, 3rd, etc.
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Open Questions / Decisions Needed
|
||||
|
||||
1. **Collaborative: Card collision handling?**
|
||||
- What if two players try to grab the same card simultaneously?
|
||||
- Option A: First one wins, second gets error toast
|
||||
- Option B: Allow both, last update wins
|
||||
- **Recommendation:** Option A for better UX
|
||||
|
||||
2. **Competitive: Show opponents' exact positions?**
|
||||
- Option A: Full transparency (see everything)
|
||||
- Option B: Only show general progress (X/N cards placed)
|
||||
- Option C: Ghost view (see positions but semi-transparent)
|
||||
- **Recommendation:** Option C
|
||||
|
||||
3. **Spectator limit?**
|
||||
- Max 10 spectators per game?
|
||||
- Performance considerations for broadcasting positions
|
||||
|
||||
4. **Replay feature?**
|
||||
- Record all position updates for playback?
|
||||
- Storage implications?
|
||||
- **Recommendation:** Future feature, not in initial scope
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Metrics
|
||||
|
||||
- **Engagement:** % of games played in multiplayer vs. solo
|
||||
- **Completion rate:** Do multiplayer games finish more/less often?
|
||||
- **Session duration:** How long do multiplayer games last?
|
||||
- **Return rate:** Do players come back for multiplayer?
|
||||
- **Social sharing:** Do players invite friends?
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Get user approval on overall plan
|
||||
2. Start with Phase 1 (player emoji on cards)
|
||||
3. Build spectator UI enhancements (Phase 2)
|
||||
4. Choose between Collaborative or Competitive for Phase 3/4
|
||||
5. Iterate based on testing and feedback
|
||||
@@ -1,143 +1,5 @@
|
||||
# Claude Code Instructions for apps/web
|
||||
|
||||
## CRITICAL: Production Dependencies
|
||||
|
||||
**NEVER add TypeScript execution tools to production dependencies.**
|
||||
|
||||
### Forbidden Production Dependencies
|
||||
|
||||
The following packages must ONLY be in `devDependencies`, NEVER in `dependencies`:
|
||||
|
||||
- ❌ `tsx` - TypeScript execution (only for scripts during development)
|
||||
- ❌ `ts-node` - TypeScript execution
|
||||
- ❌ Any TypeScript compiler/executor that runs .ts/.tsx files at runtime
|
||||
|
||||
### Why This Matters
|
||||
|
||||
1. **Docker Image Size**: These tools add 50-100MB+ to production images
|
||||
2. **Security**: Running TypeScript at runtime is a security risk
|
||||
3. **Performance**: Production should run compiled JavaScript, not interpret TypeScript
|
||||
4. **Architecture**: If you need TypeScript at runtime, the code is in the wrong place
|
||||
|
||||
### What To Do Instead
|
||||
|
||||
**❌ WRONG - Adding tsx to dependencies to run .ts/.tsx at runtime:**
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"tsx": "^4.20.5" // NEVER DO THIS
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**✅ CORRECT - Move code to proper location:**
|
||||
|
||||
1. **For Next.js API routes**: Move files to `src/` so Next.js bundles them during build
|
||||
- Example: `scripts/generateCalendar.tsx` → `src/utils/calendar/generateCalendar.tsx`
|
||||
- Next.js will compile and bundle these during `npm run build`
|
||||
|
||||
2. **For standalone scripts**: Keep in `scripts/` and use `tsx` from devDependencies
|
||||
- Only run during development/build, never at runtime
|
||||
- Scripts can use `tsx` because it's available during build
|
||||
|
||||
3. **For server-side TypeScript**: Compile to JavaScript during build
|
||||
- Use `tsc` to compile `src/` to `dist/`
|
||||
- Production runs the compiled JavaScript from `dist/`
|
||||
|
||||
### Historical Context
|
||||
|
||||
**We've made this mistake TWICE:**
|
||||
|
||||
1. **First time (commit ffae9c1b)**: Added tsx to dependencies for calendar generation scripts
|
||||
- **Fix**: Moved scripts to `src/utils/calendar/` so Next.js bundles them
|
||||
|
||||
2. **Second time (would have happened again)**: Almost added tsx again for same reason
|
||||
- **Learning**: If you're tempted to add tsx to dependencies, the architecture is wrong
|
||||
|
||||
### Red Flags
|
||||
|
||||
If you find yourself thinking:
|
||||
- "I need to add tsx to dependencies to run this .ts file in production"
|
||||
- "This script needs TypeScript at runtime"
|
||||
- "Production can't import this .tsx file"
|
||||
|
||||
**STOP.** The code is in the wrong place. Move it to `src/` for bundling.
|
||||
|
||||
### Enforcement
|
||||
|
||||
Before modifying `package.json` dependencies:
|
||||
1. Check if any TypeScript execution tools are being added
|
||||
2. Ask yourself: "Could this code be in `src/` instead?"
|
||||
3. If unsure, ask the user before proceeding
|
||||
|
||||
## CRITICAL: Code Factoring - Never Fork, Always Factor
|
||||
|
||||
**When told to share code between files, NEVER copy/paste. ALWAYS extract to shared utility.**
|
||||
|
||||
### The Mistake (Made Multiple Times)
|
||||
|
||||
When implementing addition worksheet preview examples, I was told **THREE TIMES** to factor out the problem rendering code:
|
||||
- "the example should be closely associated in the codebase semantically with the template"
|
||||
- "just be sure to factor, not fork"
|
||||
- "we need to be showing exactly what the worksheet template uses"
|
||||
|
||||
**What I did wrong:** Copied the Typst problem rendering code from `typstGenerator.ts` to `example/route.ts`
|
||||
|
||||
**Why this is wrong:**
|
||||
- Changes to worksheet layout won't reflect in preview
|
||||
- Maintaining two copies guarantees they'll drift apart
|
||||
- Violates DRY (Don't Repeat Yourself)
|
||||
- The user explicitly said "factor, not fork"
|
||||
|
||||
### What To Do Instead
|
||||
|
||||
**✅ CORRECT - Extract to shared function:**
|
||||
|
||||
1. Create shared function in `typstHelpers.ts`:
|
||||
```typescript
|
||||
export function generateProblemBoxFunction(cellSize: number): string {
|
||||
// Returns the Typst function definition that both files can use
|
||||
return `#let problem-box(problem, index) = { ... }`
|
||||
}
|
||||
```
|
||||
|
||||
2. Both `typstGenerator.ts` and `example/route.ts` import and use it:
|
||||
```typescript
|
||||
import { generateProblemBoxFunction } from './typstHelpers'
|
||||
|
||||
// In Typst template:
|
||||
${generateProblemBoxFunction(cellSize)}
|
||||
|
||||
// Then call it:
|
||||
#problem-box((a: 45, b: 27), 0)
|
||||
```
|
||||
|
||||
**❌ WRONG - Copy/paste the code:**
|
||||
```typescript
|
||||
// typstGenerator.ts
|
||||
const template = `#let problem-box = { ... }` // ← Original
|
||||
|
||||
// example/route.ts
|
||||
const template = `#let problem-box = { ... }` // ← Copy/paste = FORKED CODE
|
||||
```
|
||||
|
||||
### Red Flags
|
||||
|
||||
If you find yourself:
|
||||
- Copying large blocks of code between files
|
||||
- Saying "I'll make it match the other file"
|
||||
- Maintaining "two versions" of the same logic
|
||||
|
||||
**STOP.** Extract to a shared utility function.
|
||||
|
||||
### Rule of Thumb
|
||||
|
||||
When the user says "factor" or "share code" or "use the same template":
|
||||
1. Find the common code
|
||||
2. Extract to shared function in appropriate utility file
|
||||
3. Import and call that function from both places
|
||||
4. The shared function should be the SINGLE SOURCE OF TRUTH
|
||||
|
||||
## MANDATORY: Quality Checks for ALL Work
|
||||
|
||||
**BEFORE declaring ANY work complete, fixed, or working**, you MUST run and pass these checks:
|
||||
@@ -182,26 +44,14 @@ When asked to make ANY changes:
|
||||
1. Make your code changes
|
||||
2. Run `npm run pre-commit`
|
||||
3. If it fails, fix the issues and run again
|
||||
4. **STOP - Tell user changes are ready for testing**
|
||||
5. **WAIT for user to manually test and approve**
|
||||
6. Only commit/push when user explicitly approves or requests it
|
||||
4. Only after all checks pass can you:
|
||||
- Say the work is "done" or "complete"
|
||||
- Mark tasks as finished
|
||||
- Create commits
|
||||
- Tell the user it's working
|
||||
5. Push immediately after committing
|
||||
|
||||
**CRITICAL:** Passing `npm run pre-commit` only verifies code quality (TypeScript, linting, formatting). It does NOT verify that features work correctly. Manual testing by the user is REQUIRED before committing.
|
||||
|
||||
**Never auto-commit or auto-push after making changes.**
|
||||
|
||||
## Dev Server Management
|
||||
|
||||
**CRITICAL: The user manages running the dev server, NOT Claude Code.**
|
||||
|
||||
- ❌ DO NOT run `pnpm dev`, `npm run dev`, or `npm start`
|
||||
- ❌ DO NOT attempt to start, stop, or restart the dev server
|
||||
- ❌ DO NOT kill processes on port 3000
|
||||
- ❌ DO NOT use background Bash processes for the dev server
|
||||
- ✅ Make code changes and let the user restart the server when needed
|
||||
- ✅ You may run other commands like `npm run type-check`, `npm run lint`, etc.
|
||||
|
||||
**The user runs the dev server themselves.** The user will manually start/restart the dev server after you make changes.
|
||||
**Nothing is complete until `npm run pre-commit` passes.**
|
||||
|
||||
## Details
|
||||
|
||||
@@ -271,51 +121,6 @@ className="bg-blue-200 border-gray-300 text-brand-600"
|
||||
|
||||
See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
|
||||
|
||||
## Data Attributes for All Elements
|
||||
|
||||
**MANDATORY: All new elements MUST have data attributes for easy reference.**
|
||||
|
||||
When creating ANY new HTML/JSX element (div, button, section, etc.), add appropriate data attributes:
|
||||
|
||||
**Required patterns:**
|
||||
- `data-component="component-name"` - For top-level component containers
|
||||
- `data-element="element-name"` - For major UI elements
|
||||
- `data-section="section-name"` - For page sections
|
||||
- `data-action="action-name"` - For interactive elements (buttons, links)
|
||||
- `data-setting="setting-name"` - For game settings/config elements
|
||||
- `data-status="status-value"` - For status indicators
|
||||
|
||||
**Why this matters:**
|
||||
- Allows easy element selection for testing, debugging, and automation
|
||||
- Makes it simple to reference elements by name in discussions
|
||||
- Provides semantic meaning beyond CSS classes
|
||||
- Enables reliable E2E testing selectors
|
||||
|
||||
**Examples:**
|
||||
```typescript
|
||||
// Component container
|
||||
<div data-component="game-board" className={css({...})}>
|
||||
|
||||
// Interactive button
|
||||
<button data-action="start-game" onClick={handleStart}>
|
||||
|
||||
// Settings toggle
|
||||
<div data-setting="sound-enabled">
|
||||
|
||||
// Status indicator
|
||||
<div data-status={isOnline ? 'online' : 'offline'}>
|
||||
```
|
||||
|
||||
**DO NOT:**
|
||||
- ❌ Skip data attributes on new elements
|
||||
- ❌ Use generic names like `data-element="div"`
|
||||
- ❌ Use data attributes for styling (use CSS classes instead)
|
||||
|
||||
**DO:**
|
||||
- ✅ Use descriptive, kebab-case names
|
||||
- ✅ Add data attributes to ALL significant elements
|
||||
- ✅ Make names semantic and self-documenting
|
||||
|
||||
## Abacus Visualizations
|
||||
|
||||
**CRITICAL: This project uses @soroban/abacus-react for all abacus visualizations.**
|
||||
@@ -333,50 +138,6 @@ When creating ANY new HTML/JSX element (div, button, section, etc.), add appropr
|
||||
- ✅ Use `useAbacusConfig` for abacus configuration
|
||||
- ✅ Use `useAbacusDisplay` for reading abacus state
|
||||
|
||||
**Server-Side Rendering (CRITICAL):**
|
||||
|
||||
`AbacusReact` already supports server-side rendering - it detects SSR and disables animations automatically.
|
||||
|
||||
**✅ CORRECT - Use in build scripts:**
|
||||
```typescript
|
||||
// scripts/generateAbacusIcons.tsx
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
|
||||
const svg = renderToStaticMarkup(<AbacusReact value={5} columns={2} />)
|
||||
// This works! Scripts can use react-dom/server
|
||||
```
|
||||
|
||||
**❌ WRONG - Do NOT use in Next.js route handlers:**
|
||||
```typescript
|
||||
// src/app/icon/route.tsx - DON'T DO THIS!
|
||||
import { renderToStaticMarkup } from 'react-dom/server' // ❌ Next.js forbids this!
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
|
||||
export async function GET() {
|
||||
const svg = renderToStaticMarkup(<AbacusReact ... />) // ❌ Will fail!
|
||||
}
|
||||
```
|
||||
|
||||
**✅ CORRECT - Pre-generate and read in route handlers:**
|
||||
```typescript
|
||||
// src/app/icon/route.tsx
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
export async function GET() {
|
||||
// Read pre-generated SVG from scripts/generateAbacusIcons.tsx
|
||||
const svg = readFileSync('public/icons/day-01.svg', 'utf-8')
|
||||
return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml' } })
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern to follow:**
|
||||
1. Generate static SVGs using `scripts/generateAbacusIcons.tsx` (uses renderToStaticMarkup)
|
||||
2. Commit generated SVGs to `public/icons/` or `public/`
|
||||
3. Route handlers read and serve the pre-generated files
|
||||
4. Regenerate icons when abacus styling changes
|
||||
|
||||
**MANDATORY: Read the Docs Before Customizing**
|
||||
|
||||
**ALWAYS read the full README documentation before customizing or styling AbacusReact:**
|
||||
@@ -507,139 +268,3 @@ Before setting a z-index, always check:
|
||||
1. What stacking context is this element in?
|
||||
2. Am I comparing against siblings or global elements?
|
||||
3. Does my parent create a stacking context?
|
||||
|
||||
## Database Access
|
||||
|
||||
This project uses SQLite with Drizzle ORM. Database location: `./data/sqlite.db`
|
||||
|
||||
**ALWAYS use MCP SQLite tools for database operations:**
|
||||
- `mcp__sqlite__list_tables` - List all tables
|
||||
- `mcp__sqlite__describe_table` - Get table schema
|
||||
- `mcp__sqlite__read_query` - Run SELECT queries
|
||||
- `mcp__sqlite__write_query` - Run INSERT/UPDATE/DELETE queries
|
||||
- `mcp__sqlite__create_table` - Create new tables
|
||||
- **DO NOT use bash `sqlite3` commands** - use the MCP tools instead
|
||||
|
||||
**Database Schema:**
|
||||
- Schema definitions: `src/db/schema/`
|
||||
- Drizzle config: `drizzle.config.ts`
|
||||
- Migrations: `drizzle/` directory
|
||||
|
||||
### Creating Database Migrations
|
||||
|
||||
**CRITICAL: NEVER manually create migration SQL files or edit the journal.**
|
||||
|
||||
When adding/modifying database schema:
|
||||
|
||||
1. **Update the schema file** in `src/db/schema/`:
|
||||
```typescript
|
||||
// Example: Add new column to existing table
|
||||
export const abacusSettings = sqliteTable('abacus_settings', {
|
||||
userId: text('user_id').primaryKey(),
|
||||
// ... existing columns ...
|
||||
newField: integer('new_field', { mode: 'boolean' }).notNull().default(false),
|
||||
})
|
||||
```
|
||||
|
||||
2. **Generate migration using drizzle-kit**:
|
||||
```bash
|
||||
npx drizzle-kit generate --custom
|
||||
```
|
||||
This creates:
|
||||
- A new SQL file in `drizzle/####_name.sql`
|
||||
- Updates `drizzle/meta/_journal.json`
|
||||
- Creates a snapshot in `drizzle/meta/####_snapshot.json`
|
||||
|
||||
3. **Edit the generated SQL file** (it will be empty):
|
||||
```sql
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
ALTER TABLE `abacus_settings` ADD `new_field` integer DEFAULT 0 NOT NULL;
|
||||
```
|
||||
|
||||
4. **Test the migration** on your local database:
|
||||
```bash
|
||||
npm run db:migrate
|
||||
```
|
||||
|
||||
5. **Verify** the column was added:
|
||||
```bash
|
||||
mcp__sqlite__describe_table table_name
|
||||
```
|
||||
|
||||
**What NOT to do:**
|
||||
- ❌ DO NOT manually create SQL files in `drizzle/` without using `drizzle-kit generate`
|
||||
- ❌ DO NOT manually edit `drizzle/meta/_journal.json`
|
||||
- ❌ DO NOT run SQL directly with `sqlite3` command
|
||||
- ❌ DO NOT use `drizzle-kit generate` without `--custom` flag (it requires interactive prompts)
|
||||
|
||||
**Why this matters:**
|
||||
- Drizzle tracks applied migrations in `__drizzle_migrations` table
|
||||
- Manual SQL files won't be tracked properly
|
||||
- Production deployments run `npm run db:migrate` automatically
|
||||
- Improperly created migrations will fail in production
|
||||
|
||||
## Deployment Verification
|
||||
|
||||
**CRITICAL: Never assume deployment is complete just because the website is accessible.**
|
||||
|
||||
When monitoring deployments to production (NAS at abaci.one):
|
||||
|
||||
1. **GitHub Actions Success ≠ NAS Deployment**
|
||||
- GitHub Actions builds and pushes Docker images to GHCR
|
||||
- The NAS must separately pull and restart containers
|
||||
- There may be a delay or manual step between these
|
||||
|
||||
2. **Always verify the deployed commit:**
|
||||
```bash
|
||||
# Check what's actually running on production
|
||||
ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format="{{index .Config.Labels \"org.opencontainers.image.revision\"}}"'
|
||||
|
||||
# Or check the deployment info modal in the app UI
|
||||
# Look for the "Commit" field and compare to current HEAD
|
||||
```
|
||||
|
||||
3. **Compare commits explicitly:**
|
||||
```bash
|
||||
# Current HEAD
|
||||
git rev-parse HEAD
|
||||
|
||||
# If NAS deployed commit doesn't match HEAD, deployment is INCOMPLETE
|
||||
```
|
||||
|
||||
4. **Never report "deployed successfully" unless:**
|
||||
- ✅ GitHub Actions completed
|
||||
- ✅ NAS commit SHA matches origin/main HEAD
|
||||
- ✅ Website is accessible AND serving the new code
|
||||
|
||||
5. **If commits don't match:**
|
||||
- Report the gap clearly: "NAS is X commits behind origin/main"
|
||||
- List what features are NOT yet deployed
|
||||
- Ask if manual NAS deployment action is needed
|
||||
|
||||
**Common mistake:** Seeing https://abaci.one is online and assuming the new code is deployed. Always verify the commit SHA.
|
||||
|
||||
## Rithmomachia Game
|
||||
|
||||
When working on the Rithmomachia arcade game, refer to:
|
||||
|
||||
- **`src/arcade-games/rithmomachia/SPEC.md`** - Complete game specification
|
||||
- Official implementation spec v1
|
||||
- Board dimensions (8×16), piece types, movement rules
|
||||
- Mathematical capture relations (equality, sum, difference, multiple, divisor, product, ratio)
|
||||
- Harmony (progression) victory conditions
|
||||
- Data models, server protocol, validation logic
|
||||
- Test cases and UI/UX suggestions
|
||||
|
||||
**Quick Reference:**
|
||||
|
||||
- **Board**: 8 rows × 16 columns (A-P, 1-8)
|
||||
- **Pieces per side**: 25 total (12 Circles, 6 Triangles, 6 Squares, 1 Pyramid)
|
||||
- **Movement**: Geometric (C=diagonal, T=orthogonal, S=queen, P=king)
|
||||
- **Captures**: Mathematical relations between piece values
|
||||
- **Victory**: Harmony (3+ pieces in enemy half forming arithmetic/geometric/harmonic progression), exhaustion, or optional point threshold
|
||||
|
||||
**Critical Rules**:
|
||||
- All piece values are positive integers (use `number`, not `bigint` for game state serialization)
|
||||
- No jumping - pieces must have clear paths
|
||||
- Captures require valid mathematical relations (use helper pieces for sum/diff/product/ratio)
|
||||
- Pyramid pieces have 4 faces - face value must be chosen during relation checks
|
||||
|
||||
@@ -1,584 +0,0 @@
|
||||
# Cross-Game Stats Analysis & Universal Data Model
|
||||
|
||||
## Overview
|
||||
|
||||
This document analyzes ALL arcade games to ensure our `GameResult` type works universally.
|
||||
|
||||
## Games Analyzed
|
||||
|
||||
1. ✅ **Matching** (Memory Pairs)
|
||||
2. ✅ **Complement Race** (Math race game)
|
||||
3. ✅ **Memory Quiz** (Number memory game)
|
||||
4. ✅ **Card Sorting** (Sort abacus cards)
|
||||
5. ✅ **Rithmomachia** (Strategic board game)
|
||||
6. 🔍 **YJS Demo** (Multiplayer demo - skipping for now)
|
||||
|
||||
---
|
||||
|
||||
## Per-Game Analysis
|
||||
|
||||
### 1. Matching (Memory Pairs)
|
||||
|
||||
**Game Type**: Memory/Pattern Matching
|
||||
**Players**: 1-N (competitive multiplayer)
|
||||
**How to Win**: Most pairs matched (multiplayer) OR complete all pairs (solo)
|
||||
|
||||
**Data Tracked**:
|
||||
```typescript
|
||||
{
|
||||
scores: { [playerId]: matchCount }
|
||||
moves: number
|
||||
matchedPairs: number
|
||||
totalPairs: number
|
||||
gameTime: milliseconds
|
||||
accuracy: percentage (matchedPairs / moves * 100)
|
||||
grade: 'A+' | 'A' | 'B+' | ...
|
||||
starRating: 1-5
|
||||
}
|
||||
```
|
||||
|
||||
**Winner Determination**:
|
||||
- Solo: completed = won
|
||||
- Multiplayer: highest score wins
|
||||
|
||||
**Fits GameResult?** ✅
|
||||
```typescript
|
||||
{
|
||||
gameType: 'matching',
|
||||
duration: gameTime,
|
||||
playerResults: [{
|
||||
playerId,
|
||||
won: isWinner,
|
||||
score: matchCount,
|
||||
accuracy: 0.0-1.0,
|
||||
metrics: { moves, matchedPairs, difficulty }
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Complement Race
|
||||
|
||||
**Game Type**: Racing/Quiz hybrid
|
||||
**Players**: 1-N (competitive race)
|
||||
**How to Win**: Highest score OR reach finish line first (depending on mode)
|
||||
|
||||
**Data Tracked**:
|
||||
```typescript
|
||||
{
|
||||
players: {
|
||||
[playerId]: {
|
||||
score: number
|
||||
streak: number
|
||||
bestStreak: number
|
||||
correctAnswers: number
|
||||
totalQuestions: number
|
||||
position: 0-100% (for practice/survival)
|
||||
deliveredPassengers: number (sprint mode)
|
||||
}
|
||||
}
|
||||
gameTime: milliseconds
|
||||
winner: playerId | null
|
||||
leaderboard: [{ playerId, score, rank }]
|
||||
}
|
||||
```
|
||||
|
||||
**Winner Determination**:
|
||||
- Practice/Survival: reach 100% position
|
||||
- Sprint: highest score (delivered passengers)
|
||||
|
||||
**Fits GameResult?** ✅
|
||||
```typescript
|
||||
{
|
||||
gameType: 'complement-race',
|
||||
duration: gameTime,
|
||||
playerResults: [{
|
||||
playerId,
|
||||
won: winnerId === playerId,
|
||||
score: player.score,
|
||||
accuracy: player.correctAnswers / player.totalQuestions,
|
||||
placement: leaderboard rank,
|
||||
metrics: {
|
||||
streak: player.bestStreak,
|
||||
correctAnswers: player.correctAnswers,
|
||||
totalQuestions: player.totalQuestions
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Memory Quiz
|
||||
|
||||
**Game Type**: Memory/Recall
|
||||
**Players**: 1-N (cooperative OR competitive)
|
||||
**How to Win**:
|
||||
- Cooperative: team finds all numbers
|
||||
- Competitive: most correct answers
|
||||
|
||||
**Data Tracked**:
|
||||
```typescript
|
||||
{
|
||||
playerScores: {
|
||||
[playerId]: { correct: number, incorrect: number }
|
||||
}
|
||||
foundNumbers: number[]
|
||||
correctAnswers: number[]
|
||||
selectedCount: 2 | 5 | 8 | 12 | 15
|
||||
playMode: 'cooperative' | 'competitive'
|
||||
gameTime: milliseconds
|
||||
}
|
||||
```
|
||||
|
||||
**Winner Determination**:
|
||||
- Cooperative: ALL found = team wins
|
||||
- Competitive: highest correct count wins
|
||||
|
||||
**Fits GameResult?** ✅ **BUT needs special handling for cooperative**
|
||||
```typescript
|
||||
{
|
||||
gameType: 'memory-quiz',
|
||||
duration: gameTime,
|
||||
playerResults: [{
|
||||
playerId,
|
||||
won: playMode === 'cooperative'
|
||||
? foundAll // All players win or lose together
|
||||
: hasHighestScore, // Individual winner
|
||||
score: playerScores[playerId].correct,
|
||||
accuracy: correct / (correct + incorrect),
|
||||
metrics: {
|
||||
correct: playerScores[playerId].correct,
|
||||
incorrect: playerScores[playerId].incorrect,
|
||||
difficulty: selectedCount
|
||||
}
|
||||
}],
|
||||
metadata: {
|
||||
playMode: 'cooperative' | 'competitive',
|
||||
isTeamVictory: boolean // ← IMPORTANT for cooperative games
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**NEW INSIGHT**: Cooperative games need special handling - all players share win/loss!
|
||||
|
||||
---
|
||||
|
||||
### 4. Card Sorting
|
||||
|
||||
**Game Type**: Sorting/Puzzle
|
||||
**Players**: 1-N (solo, collaborative, competitive, relay)
|
||||
**How to Win**:
|
||||
- Solo: achieve high score (0-100)
|
||||
- Collaborative: team achieves score
|
||||
- Competitive: highest individual score
|
||||
- Relay: TBD (not fully implemented)
|
||||
|
||||
**Data Tracked**:
|
||||
```typescript
|
||||
{
|
||||
scoreBreakdown: {
|
||||
finalScore: 0-100
|
||||
exactMatches: number
|
||||
lcsLength: number // Longest common subsequence
|
||||
inversions: number // Out-of-order pairs
|
||||
relativeOrderScore: 0-100
|
||||
exactPositionScore: 0-100
|
||||
inversionScore: 0-100
|
||||
elapsedTime: seconds
|
||||
}
|
||||
gameMode: 'solo' | 'collaborative' | 'competitive' | 'relay'
|
||||
}
|
||||
```
|
||||
|
||||
**Winner Determination**:
|
||||
- Solo/Collaborative: score > threshold (e.g., 70+)
|
||||
- Competitive: highest score
|
||||
|
||||
**Fits GameResult?** ✅ **Similar to Memory Quiz**
|
||||
```typescript
|
||||
{
|
||||
gameType: 'card-sorting',
|
||||
duration: elapsedTime * 1000,
|
||||
playerResults: [{
|
||||
playerId,
|
||||
won: gameMode === 'collaborative'
|
||||
? scoreBreakdown.finalScore >= 70 // Team threshold
|
||||
: hasHighestScore,
|
||||
score: scoreBreakdown.finalScore,
|
||||
accuracy: scoreBreakdown.exactMatches / cardCount,
|
||||
metrics: {
|
||||
exactMatches: scoreBreakdown.exactMatches,
|
||||
inversions: scoreBreakdown.inversions,
|
||||
lcsLength: scoreBreakdown.lcsLength
|
||||
}
|
||||
}],
|
||||
metadata: {
|
||||
gameMode,
|
||||
isTeamVictory: gameMode === 'collaborative'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Rithmomachia
|
||||
|
||||
**Game Type**: Strategic board game (2-player only)
|
||||
**Players**: Exactly 2 (White vs Black)
|
||||
**How to Win**: Multiple victory conditions (harmony, points, exhaustion, resignation)
|
||||
|
||||
**Data Tracked**:
|
||||
```typescript
|
||||
{
|
||||
winner: 'W' | 'B' | null
|
||||
winCondition: 'HARMONY' | 'EXHAUSTION' | 'RESIGNATION' | 'POINTS' | ...
|
||||
capturedPieces: { W: Piece[], B: Piece[] }
|
||||
pointsCaptured: { W: number, B: number }
|
||||
history: MoveRecord[]
|
||||
gameTime: milliseconds (computed from history)
|
||||
}
|
||||
```
|
||||
|
||||
**Winner Determination**:
|
||||
- Specific win condition triggered
|
||||
- No draws (or rare)
|
||||
|
||||
**Fits GameResult?** ✅ **Needs win condition metadata**
|
||||
```typescript
|
||||
{
|
||||
gameType: 'rithmomachia',
|
||||
duration: gameTime,
|
||||
playerResults: [
|
||||
{
|
||||
playerId: whitePlayerId,
|
||||
won: winner === 'W',
|
||||
score: capturedPieces.W.length, // or pointsCaptured.W
|
||||
metrics: {
|
||||
capturedPieces: capturedPieces.W.length,
|
||||
points: pointsCaptured?.W || 0,
|
||||
moves: history.filter(m => m.color === 'W').length
|
||||
}
|
||||
},
|
||||
{
|
||||
playerId: blackPlayerId,
|
||||
won: winner === 'B',
|
||||
score: capturedPieces.B.length,
|
||||
metrics: {
|
||||
capturedPieces: capturedPieces.B.length,
|
||||
points: pointsCaptured?.B || 0,
|
||||
moves: history.filter(m => m.color === 'B').length
|
||||
}
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
winCondition: 'HARMONY' | 'POINTS' | ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-Game Patterns Identified
|
||||
|
||||
### Pattern 1: Competitive (Most Common)
|
||||
**Games**: Matching (multiplayer), Complement Race, Memory Quiz (competitive), Card Sorting (competitive)
|
||||
|
||||
**Characteristics**:
|
||||
- Each player has their own score
|
||||
- Winner = highest score
|
||||
- Players track individually
|
||||
|
||||
**Stats to track per player**:
|
||||
- games_played ++
|
||||
- wins ++ (if winner)
|
||||
- losses ++ (if not winner)
|
||||
- best_time (if faster)
|
||||
- highest_accuracy (if better)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Cooperative (Team-Based)
|
||||
**Games**: Memory Quiz (cooperative), Card Sorting (collaborative)
|
||||
|
||||
**Characteristics**:
|
||||
- All players share outcome
|
||||
- Team wins or loses together
|
||||
- Individual contributions still tracked
|
||||
|
||||
**Stats to track per player**:
|
||||
- games_played ++
|
||||
- wins ++ (if TEAM won) ← Key difference
|
||||
- losses ++ (if TEAM lost)
|
||||
- Individual metrics still tracked (correct answers, etc.)
|
||||
|
||||
**CRITICAL**: Check `metadata.isTeamVictory` to determine if all players get same win/loss!
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Head-to-Head (Exactly 2 Players)
|
||||
**Games**: Rithmomachia
|
||||
|
||||
**Characteristics**:
|
||||
- Always 2 players
|
||||
- One wins, one loses (rare draws)
|
||||
- Different win conditions
|
||||
|
||||
**Stats to track per player**:
|
||||
- games_played ++
|
||||
- wins ++ (winner only)
|
||||
- losses ++ (loser only)
|
||||
- Game-specific metrics (captures, harmonies)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 4: Solo Completion
|
||||
**Games**: Matching (solo), Complement Race (practice), Memory Quiz (solo), Card Sorting (solo)
|
||||
|
||||
**Characteristics**:
|
||||
- Single player
|
||||
- Win = completion or threshold
|
||||
- Compete against self/time
|
||||
|
||||
**Stats to track**:
|
||||
- games_played ++
|
||||
- wins ++ (if completed/threshold met)
|
||||
- losses ++ (if failed/gave up)
|
||||
- best_time, highest_accuracy
|
||||
|
||||
---
|
||||
|
||||
## Refined Universal Data Model
|
||||
|
||||
### GameResult Type (UPDATED)
|
||||
|
||||
```typescript
|
||||
export interface GameResult {
|
||||
// Game identification
|
||||
gameType: string // e.g., "matching", "complement-race", etc.
|
||||
|
||||
// Player results (supports 1-N players)
|
||||
playerResults: PlayerGameResult[]
|
||||
|
||||
// Timing
|
||||
completedAt: number // timestamp
|
||||
duration: number // milliseconds
|
||||
|
||||
// Optional game-specific data
|
||||
metadata?: {
|
||||
// For cooperative games
|
||||
isTeamVictory?: boolean // ← NEW: all players share win/loss
|
||||
|
||||
// For specific win conditions
|
||||
winCondition?: string // e.g., "HARMONY", "POINTS", "TIMEOUT"
|
||||
|
||||
// For game modes
|
||||
gameMode?: string // e.g., "solo", "competitive", "cooperative"
|
||||
|
||||
// Any other game-specific info
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlayerGameResult {
|
||||
playerId: string
|
||||
|
||||
// Outcome
|
||||
won: boolean // For cooperative: all players same value
|
||||
placement?: number // 1st, 2nd, 3rd (for competitive with >2 players)
|
||||
|
||||
// Performance
|
||||
score?: number
|
||||
accuracy?: number // 0.0 - 1.0
|
||||
completionTime?: number // milliseconds (player-specific time)
|
||||
|
||||
// Game-specific metrics (optional, stored as JSON in DB)
|
||||
metrics?: {
|
||||
// Matching
|
||||
moves?: number
|
||||
matchedPairs?: number
|
||||
difficulty?: number
|
||||
|
||||
// Complement Race
|
||||
streak?: number
|
||||
correctAnswers?: number
|
||||
totalQuestions?: number
|
||||
|
||||
// Memory Quiz
|
||||
correct?: number
|
||||
incorrect?: number
|
||||
|
||||
// Card Sorting
|
||||
exactMatches?: number
|
||||
inversions?: number
|
||||
lcsLength?: number
|
||||
|
||||
// Rithmomachia
|
||||
capturedPieces?: number
|
||||
points?: number
|
||||
|
||||
// Extensible for future games
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stats Recording Logic (UPDATED)
|
||||
|
||||
### For Each Player in GameResult
|
||||
|
||||
```typescript
|
||||
// Fetch player stats
|
||||
const stats = await getPlayerStats(playerId)
|
||||
|
||||
// Always increment
|
||||
stats.gamesPlayed++
|
||||
|
||||
// Handle wins/losses based on game type
|
||||
if (gameResult.metadata?.isTeamVictory !== undefined) {
|
||||
// COOPERATIVE: All players share outcome
|
||||
if (playerResult.won) {
|
||||
stats.totalWins++
|
||||
} else {
|
||||
stats.totalLosses++
|
||||
}
|
||||
} else {
|
||||
// COMPETITIVE/SOLO: Individual outcome
|
||||
if (playerResult.won) {
|
||||
stats.totalWins++
|
||||
} else {
|
||||
stats.totalLosses++
|
||||
}
|
||||
}
|
||||
|
||||
// Update performance metrics
|
||||
if (playerResult.completionTime && (
|
||||
!stats.bestTime || playerResult.completionTime < stats.bestTime
|
||||
)) {
|
||||
stats.bestTime = playerResult.completionTime
|
||||
}
|
||||
|
||||
if (playerResult.accuracy && playerResult.accuracy > stats.highestAccuracy) {
|
||||
stats.highestAccuracy = playerResult.accuracy
|
||||
}
|
||||
|
||||
// Update per-game stats (JSON)
|
||||
stats.gameStats[gameResult.gameType] = {
|
||||
gamesPlayed: (stats.gameStats[gameResult.gameType]?.gamesPlayed || 0) + 1,
|
||||
wins: (stats.gameStats[gameResult.gameType]?.wins || 0) + (playerResult.won ? 1 : 0),
|
||||
// ... other game-specific aggregates
|
||||
}
|
||||
|
||||
// Update favorite game type (most played)
|
||||
stats.favoriteGameType = getMostPlayedGame(stats.gameStats)
|
||||
|
||||
// Update timestamps
|
||||
stats.lastPlayedAt = gameResult.completedAt
|
||||
stats.updatedAt = Date.now()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema (CONFIRMED)
|
||||
|
||||
No changes needed from original design! The `metrics` JSON field handles game-specific data perfectly.
|
||||
|
||||
```sql
|
||||
CREATE TABLE player_stats (
|
||||
player_id TEXT PRIMARY KEY,
|
||||
|
||||
-- Aggregates
|
||||
games_played INTEGER NOT NULL DEFAULT 0,
|
||||
total_wins INTEGER NOT NULL DEFAULT 0,
|
||||
total_losses INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Performance
|
||||
best_time INTEGER,
|
||||
highest_accuracy REAL NOT NULL DEFAULT 0,
|
||||
|
||||
-- Per-game breakdown (JSON)
|
||||
game_stats TEXT NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Meta
|
||||
favorite_game_type TEXT,
|
||||
last_played_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Insights & Design Decisions
|
||||
|
||||
### 1. Cooperative Games Need Special Flag
|
||||
**Problem**: Memory Quiz (cooperative) and Card Sorting (collaborative) - all players share win/loss.
|
||||
|
||||
**Solution**: Add `metadata.isTeamVictory: boolean` to `GameResult`. When `true`, recording logic gives ALL players the same win/loss.
|
||||
|
||||
### 2. Flexible Metrics Field
|
||||
**Problem**: Each game tracks different metrics (moves, streak, inversions, etc.).
|
||||
|
||||
**Solution**: `PlayerGameResult.metrics` is an open object. Store game-specific data here, saved as JSON in DB.
|
||||
|
||||
### 3. Placement for Tournaments
|
||||
**Problem**: 3+ player games need to track ranking (1st, 2nd, 3rd).
|
||||
|
||||
**Solution**: `PlayerGameResult.placement` field. Useful for leaderboards.
|
||||
|
||||
### 4. Win Conditions Matter
|
||||
**Problem**: Rithmomachia has multiple win conditions (harmony, points, etc.).
|
||||
|
||||
**Solution**: `metadata.winCondition` stores how the game was won. Useful for achievements/stats breakdown.
|
||||
|
||||
### 5. Score is Optional
|
||||
**Problem**: Not all games have scores (e.g., Rithmomachia can win by harmony without points enabled).
|
||||
|
||||
**Solution**: Make `score` optional. Use `won` as primary outcome indicator.
|
||||
|
||||
---
|
||||
|
||||
## Testing Matrix
|
||||
|
||||
### Scenarios to Test
|
||||
|
||||
| Game | Mode | Players | Expected Outcome |
|
||||
|------|------|---------|------------------|
|
||||
| Matching | Solo | 1 | Player wins if completed |
|
||||
| Matching | Competitive | 2+ | Winner = highest score, others lose |
|
||||
| Complement Race | Sprint | 2+ | Winner = highest score |
|
||||
| Memory Quiz | Cooperative | 2+ | ALL win or ALL lose (team) |
|
||||
| Memory Quiz | Competitive | 2+ | Winner = most correct |
|
||||
| Card Sorting | Solo | 1 | Win if score >= 70 |
|
||||
| Card Sorting | Collaborative | 2+ | ALL win or ALL lose (team) |
|
||||
| Card Sorting | Competitive | 2+ | Winner = highest score |
|
||||
| Rithmomachia | PvP | 2 | One wins (by condition), one loses |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
✅ **Universal `GameResult` type CONFIRMED to work for all games**
|
||||
|
||||
**Key Requirements**:
|
||||
1. Support 1-N players (flexible array)
|
||||
2. Support cooperative games (isTeamVictory flag)
|
||||
3. Support game-specific metrics (open metrics object)
|
||||
4. Support multiple win conditions (winCondition metadata)
|
||||
5. Track both individual AND team performance
|
||||
|
||||
**Next Steps**:
|
||||
1. Update `.claude/PER_PLAYER_STATS_ARCHITECTURE.md` with refined types
|
||||
2. Implement database schema
|
||||
3. Build API endpoints
|
||||
4. Create React hooks
|
||||
5. Integrate with each game (starting with Matching)
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Complete cross-game analysis
|
||||
**Result**: GameResult type is universal and robust
|
||||
**Date**: 2025-01-03
|
||||
@@ -1,468 +0,0 @@
|
||||
# Google Classroom Integration Setup Guide
|
||||
|
||||
**Goal:** Set up Google Classroom API integration using mostly CLI commands, minimizing web console interaction.
|
||||
|
||||
**Time Required:** 15-20 minutes
|
||||
**Cost:** $0 (free for educational use)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
✅ **gcloud CLI installed** (already installed at `/opt/homebrew/bin/gcloud`)
|
||||
✅ **Valid Google account**
|
||||
- **Billing account** (required by Google, but FREE for Classroom API)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (TL;DR)
|
||||
|
||||
```bash
|
||||
# Run the automated setup script
|
||||
./scripts/setup-google-classroom.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Authenticate with your Google account
|
||||
2. Create a GCP project
|
||||
3. Enable Classroom & People APIs
|
||||
4. Guide you through OAuth setup (2 web console steps)
|
||||
5. Configure your `.env.local` file
|
||||
|
||||
**Note:** Steps 6 & 7 still require web console (Google doesn't provide CLI for OAuth consent screen), but the script opens the pages for you and provides exact instructions.
|
||||
|
||||
---
|
||||
|
||||
## What the Script Does (Step by Step)
|
||||
|
||||
### 1. Authentication ✅ Fully Automated
|
||||
```bash
|
||||
gcloud auth login
|
||||
```
|
||||
Opens browser, you log in with Google, done.
|
||||
|
||||
### 2. Create GCP Project ✅ Fully Automated
|
||||
```bash
|
||||
PROJECT_ID="soroban-abacus-$(date +%s)" # Unique ID with timestamp
|
||||
gcloud projects create "$PROJECT_ID" --name="Soroban Abacus Flashcards"
|
||||
gcloud config set project "$PROJECT_ID"
|
||||
```
|
||||
|
||||
### 3. Link Billing Account ✅ Mostly Automated
|
||||
```bash
|
||||
# List your billing accounts
|
||||
gcloud billing accounts list
|
||||
|
||||
# Link to project
|
||||
gcloud billing projects link "$PROJECT_ID" --billing-account="BILLING_ACCOUNT_ID"
|
||||
```
|
||||
|
||||
**Why billing is required:**
|
||||
- Google requires billing for API access (even free APIs!)
|
||||
- Classroom API is **FREE** with no usage charges
|
||||
- You won't be charged unless you enable paid services
|
||||
|
||||
**If you don't have a billing account:**
|
||||
- Script will prompt you to create one at: https://console.cloud.google.com/billing
|
||||
- It's quick: just add payment method (won't be charged)
|
||||
- Press Enter in terminal after creation
|
||||
|
||||
### 4. Enable APIs ✅ Fully Automated
|
||||
```bash
|
||||
gcloud services enable classroom.googleapis.com
|
||||
gcloud services enable people.googleapis.com
|
||||
```
|
||||
|
||||
Takes 1-2 minutes to propagate.
|
||||
|
||||
### 5. Create OAuth Credentials ⚠️ Requires Web Console
|
||||
|
||||
**Why CLI doesn't work:**
|
||||
Google doesn't provide `gcloud` commands for creating OAuth clients. You need the web console.
|
||||
|
||||
**What the script does:**
|
||||
- Opens: https://console.cloud.google.com/apis/credentials?project=YOUR_PROJECT
|
||||
- Provides exact instructions (copy-paste ready)
|
||||
|
||||
**Manual steps (takes 2 minutes):**
|
||||
1. Click "**Create Credentials**" → "**OAuth client ID**"
|
||||
2. Application type: **"Web application"**
|
||||
3. Name: **"Soroban Abacus Web"**
|
||||
4. **Authorized JavaScript origins:**
|
||||
```
|
||||
http://localhost:3000
|
||||
https://abaci.one
|
||||
```
|
||||
5. **Authorized redirect URIs:**
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/google
|
||||
https://abaci.one/api/auth/callback/google
|
||||
```
|
||||
6. Click **"Create"**
|
||||
7. **Copy** the Client ID and Client Secret (you'll paste into terminal)
|
||||
|
||||
### 6. Configure OAuth Consent Screen ⚠️ Requires Web Console
|
||||
|
||||
**Why CLI doesn't work:**
|
||||
OAuth consent screen configuration is web-only.
|
||||
|
||||
**What the script does:**
|
||||
- Opens: https://console.cloud.google.com/apis/credentials/consent?project=YOUR_PROJECT
|
||||
- Provides step-by-step instructions
|
||||
|
||||
**Manual steps (takes 3 minutes):**
|
||||
|
||||
**Screen 1: OAuth consent screen**
|
||||
- User Type: **"External"** (unless you have Google Workspace)
|
||||
- Click "**Create**"
|
||||
|
||||
**Screen 2: App information**
|
||||
- App name: **"Soroban Abacus Flashcards"**
|
||||
- User support email: **Your email**
|
||||
- App logo: (optional)
|
||||
- App domain: (optional, can add later)
|
||||
- Developer contact: **Your email**
|
||||
- Click "**Save and Continue**"
|
||||
|
||||
**Screen 3: Scopes**
|
||||
- Click "**Add or Remove Scopes**"
|
||||
- Filter/search for these scopes and check them:
|
||||
- ✅ `.../auth/userinfo.email` (See your primary Google Account email)
|
||||
- ✅ `.../auth/userinfo.profile` (See your personal info)
|
||||
- ✅ `.../auth/classroom.courses.readonly` (View courses)
|
||||
- ✅ `.../auth/classroom.rosters.readonly` (View class rosters)
|
||||
- Click "**Update**"
|
||||
- Click "**Save and Continue**"
|
||||
|
||||
**Screen 4: Test users**
|
||||
- Click "**Add Users**"
|
||||
- Add your email address (for testing)
|
||||
- Click "**Save and Continue**"
|
||||
|
||||
**Screen 5: Summary**
|
||||
- Review and click "**Back to Dashboard**"
|
||||
|
||||
Done! ✅
|
||||
|
||||
### 7. Save Credentials to .env.local ✅ Fully Automated
|
||||
|
||||
Script prompts you for:
|
||||
- Client ID (paste from step 5)
|
||||
- Client Secret (paste from step 5)
|
||||
|
||||
Then automatically adds to `.env.local`:
|
||||
```bash
|
||||
# Google OAuth (Generated by setup-google-classroom.sh)
|
||||
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
||||
GOOGLE_CLIENT_SECRET="GOCSPX-your-secret"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## After Running the Script
|
||||
|
||||
### Verify Setup
|
||||
|
||||
```bash
|
||||
# Check project configuration
|
||||
gcloud config get-value project
|
||||
|
||||
# List enabled APIs
|
||||
gcloud services list --enabled
|
||||
|
||||
# Check Classroom API is enabled
|
||||
gcloud services list --enabled | grep classroom
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
classroom.googleapis.com Google Classroom API
|
||||
```
|
||||
|
||||
### Test API Access
|
||||
|
||||
```bash
|
||||
# Get an access token
|
||||
gcloud auth application-default login
|
||||
gcloud auth application-default print-access-token
|
||||
|
||||
# Test Classroom API (replace TOKEN)
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://classroom.googleapis.com/v1/courses
|
||||
```
|
||||
|
||||
Expected response (if you have no courses yet):
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NextAuth Configuration
|
||||
|
||||
Now that you have credentials, add Google provider to NextAuth:
|
||||
|
||||
### 1. Check Current NextAuth Config
|
||||
|
||||
```bash
|
||||
cat src/app/api/auth/[...nextauth]/route.ts
|
||||
```
|
||||
|
||||
### 2. Add Google Provider
|
||||
|
||||
Add to your NextAuth providers array:
|
||||
|
||||
```typescript
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
authorization: {
|
||||
params: {
|
||||
scope: [
|
||||
'openid',
|
||||
'email',
|
||||
'profile',
|
||||
'https://www.googleapis.com/auth/classroom.courses.readonly',
|
||||
'https://www.googleapis.com/auth/classroom.rosters.readonly',
|
||||
].join(' '),
|
||||
prompt: 'consent',
|
||||
access_type: 'offline',
|
||||
response_type: 'code'
|
||||
}
|
||||
}
|
||||
}),
|
||||
// ... your existing providers
|
||||
],
|
||||
// ... rest of config
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Test Login
|
||||
|
||||
```bash
|
||||
# Start dev server
|
||||
npm run dev
|
||||
|
||||
# Open browser
|
||||
open http://localhost:3000
|
||||
```
|
||||
|
||||
Click "Sign in with Google" and verify:
|
||||
- ✅ OAuth consent screen appears
|
||||
- ✅ Shows requested permissions
|
||||
- ✅ Successfully logs in
|
||||
- ✅ User profile is created
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Billing account required"
|
||||
|
||||
**Problem:** Can't enable APIs without billing
|
||||
**Solution:** Create billing account at https://console.cloud.google.com/billing
|
||||
- Won't be charged for Classroom API (it's free)
|
||||
- Just need payment method on file
|
||||
|
||||
### "Error 401: deleted_client"
|
||||
|
||||
**Problem:** OAuth client was deleted or not created properly
|
||||
**Solution:** Re-run OAuth client creation (step 5)
|
||||
```bash
|
||||
open "https://console.cloud.google.com/apis/credentials?project=$(gcloud config get-value project)"
|
||||
```
|
||||
|
||||
### "Error 403: Access Not Configured"
|
||||
|
||||
**Problem:** APIs not enabled yet (takes 1-2 min to propagate)
|
||||
**Solution:** Wait 2 minutes, then verify:
|
||||
```bash
|
||||
gcloud services list --enabled | grep classroom
|
||||
```
|
||||
|
||||
### "Invalid redirect URI"
|
||||
|
||||
**Problem:** Redirect URI doesn't match OAuth client config
|
||||
**Solution:** Check that these URIs are in your OAuth client:
|
||||
- http://localhost:3000/api/auth/callback/google
|
||||
- https://abaci.one/api/auth/callback/google
|
||||
|
||||
### "App is not verified"
|
||||
|
||||
**Problem:** OAuth consent screen in "Testing" mode
|
||||
**Solution:** This is **normal** for development!
|
||||
- Click "Advanced" → "Go to [app name] (unsafe)"
|
||||
- Only affects external test users
|
||||
- For production, submit for verification (takes 1-2 weeks)
|
||||
|
||||
---
|
||||
|
||||
## CLI Reference
|
||||
|
||||
### Project Management
|
||||
|
||||
```bash
|
||||
# List all your projects
|
||||
gcloud projects list
|
||||
|
||||
# Switch project
|
||||
gcloud config set project PROJECT_ID
|
||||
|
||||
# Delete project (if needed)
|
||||
gcloud projects delete PROJECT_ID
|
||||
```
|
||||
|
||||
### API Management
|
||||
|
||||
```bash
|
||||
# List enabled APIs
|
||||
gcloud services list --enabled
|
||||
|
||||
# Enable an API
|
||||
gcloud services enable APINAME.googleapis.com
|
||||
|
||||
# Disable an API
|
||||
gcloud services disable APINAME.googleapis.com
|
||||
|
||||
# Check quota
|
||||
gcloud services quota describe classroom.googleapis.com
|
||||
```
|
||||
|
||||
### OAuth Management
|
||||
|
||||
```bash
|
||||
# List OAuth clients (requires REST API)
|
||||
PROJECT_ID=$(gcloud config get-value project)
|
||||
ACCESS_TOKEN=$(gcloud auth application-default print-access-token)
|
||||
|
||||
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
"https://oauth2.googleapis.com/v1/projects/$PROJECT_ID/oauthClients"
|
||||
```
|
||||
|
||||
### Billing
|
||||
|
||||
```bash
|
||||
# List billing accounts
|
||||
gcloud billing accounts list
|
||||
|
||||
# Link billing to project
|
||||
gcloud billing projects link PROJECT_ID --billing-account=ACCOUNT_ID
|
||||
|
||||
# Check project billing status
|
||||
gcloud billing projects describe PROJECT_ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What You Can Do From CLI (Summary)
|
||||
|
||||
✅ **Fully Automated:**
|
||||
- Authenticate with Google
|
||||
- Create GCP project
|
||||
- Enable APIs
|
||||
- Link billing account
|
||||
- Configure environment variables
|
||||
|
||||
⚠️ **Requires Web Console (2-5 minutes):**
|
||||
- Create OAuth client (2 min)
|
||||
- Configure OAuth consent screen (3 min)
|
||||
|
||||
**Why web console required:**
|
||||
Google doesn't provide CLI for these security-sensitive operations. But the script:
|
||||
- Opens the exact pages for you
|
||||
- Provides step-by-step instructions
|
||||
- Makes it as painless as possible
|
||||
|
||||
---
|
||||
|
||||
## Cost Breakdown
|
||||
|
||||
| Item | Cost |
|
||||
|------|------|
|
||||
| GCP project | $0 |
|
||||
| Google Classroom API | $0 (free forever) |
|
||||
| Google People API | $0 (free forever) |
|
||||
| Billing account requirement | $0 (no charges) |
|
||||
| **Total** | **$0** |
|
||||
|
||||
**Note:** You need to add a payment method for billing account, but Google Classroom API is completely free with no usage limits.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Setup
|
||||
|
||||
1. ✅ Run the setup script: `./scripts/setup-google-classroom.sh`
|
||||
2. ✅ Add Google provider to NextAuth
|
||||
3. ✅ Test "Sign in with Google"
|
||||
4. 📝 Implement class import feature (Phase 2 of roadmap)
|
||||
5. 📝 Build teacher dashboard
|
||||
6. 📝 Add assignment integration
|
||||
|
||||
Refer to `.claude/PLATFORM_INTEGRATION_ROADMAP.md` for full implementation timeline.
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Protect Your Secrets
|
||||
|
||||
```bash
|
||||
# Check .env.local is in .gitignore
|
||||
cat .gitignore | grep .env.local
|
||||
```
|
||||
|
||||
Should see:
|
||||
```
|
||||
.env*.local
|
||||
```
|
||||
|
||||
### Rotate Credentials Periodically
|
||||
|
||||
```bash
|
||||
# Open credentials page
|
||||
PROJECT_ID=$(gcloud config get-value project)
|
||||
open "https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID"
|
||||
|
||||
# Delete old client, create new one
|
||||
# Update .env.local with new credentials
|
||||
```
|
||||
|
||||
### Use Different Credentials for Dev/Prod
|
||||
|
||||
**Development:**
|
||||
- OAuth client: `http://localhost:3000/api/auth/callback/google`
|
||||
- Test users only
|
||||
|
||||
**Production:**
|
||||
- OAuth client: `https://abaci.one/api/auth/callback/google`
|
||||
- Verified app (submit for review)
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**Official Documentation:**
|
||||
- GCP CLI: https://cloud.google.com/sdk/gcloud
|
||||
- Classroom API: https://developers.google.com/classroom
|
||||
- OAuth 2.0: https://developers.google.com/identity/protocols/oauth2
|
||||
|
||||
**Script Location:**
|
||||
- `scripts/setup-google-classroom.sh`
|
||||
|
||||
**Configuration Files:**
|
||||
- `.env.local` (credentials)
|
||||
- `src/app/api/auth/[...nextauth]/route.ts` (NextAuth config)
|
||||
|
||||
---
|
||||
|
||||
**Ready to run?**
|
||||
|
||||
```bash
|
||||
./scripts/setup-google-classroom.sh
|
||||
```
|
||||
|
||||
Good luck! 🚀
|
||||
@@ -1,283 +0,0 @@
|
||||
# Matching Game Stats Integration Guide
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Files to modify**: `src/arcade-games/matching/components/ResultsPhase.tsx`
|
||||
|
||||
**What we're adding**: Call `useRecordGameResult()` when game completes to save per-player stats.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### ResultsPhase.tsx (lines 9-29)
|
||||
|
||||
Already has all the data we need:
|
||||
|
||||
```typescript
|
||||
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
|
||||
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
|
||||
|
||||
const gameTime = state.gameEndTime && state.gameStartTime
|
||||
? state.gameEndTime - state.gameStartTime
|
||||
: 0
|
||||
|
||||
const analysis = getPerformanceAnalysis(state)
|
||||
const multiplayerResult = gameMode === 'multiplayer'
|
||||
? getMultiplayerWinner(state, activePlayers)
|
||||
: null
|
||||
```
|
||||
|
||||
**Available data:**
|
||||
- ✅ `state.scores` - scores by player ID
|
||||
- ✅ `state.gameStartTime`, `state.gameEndTime` - timing
|
||||
- ✅ `state.matchedPairs`, `state.totalPairs` - completion
|
||||
- ✅ `state.moves` - total moves
|
||||
- ✅ `activePlayers` - array of player IDs
|
||||
- ✅ `multiplayerResult.winners` - who won
|
||||
- ✅ `analysis.statistics.accuracy` - accuracy percentage
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Add state flag to prevent duplicate recording
|
||||
|
||||
Add `recorded: boolean` to `MatchingState` type:
|
||||
|
||||
```typescript
|
||||
// src/arcade-games/matching/types.ts (add to MatchingState interface)
|
||||
|
||||
export interface MatchingState extends GameState {
|
||||
// ... existing fields ...
|
||||
|
||||
// Stats recording
|
||||
recorded?: boolean // ← ADD THIS
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Import the hook in ResultsPhase.tsx
|
||||
|
||||
```typescript
|
||||
// At top of src/arcade-games/matching/components/ResultsPhase.tsx
|
||||
|
||||
import { useEffect } from 'react' // ← ADD if not present
|
||||
import { useRecordGameResult } from '@/hooks/useRecordGameResult'
|
||||
import type { GameResult } from '@/lib/arcade/stats/types'
|
||||
```
|
||||
|
||||
### Step 3: Call the hook
|
||||
|
||||
```typescript
|
||||
// Inside ResultsPhase component, after existing hooks
|
||||
|
||||
export function ResultsPhase() {
|
||||
const router = useRouter()
|
||||
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
|
||||
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
|
||||
|
||||
// ← ADD THIS
|
||||
const { mutate: recordGame, isPending: isRecording } = useRecordGameResult()
|
||||
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
### Step 4: Record game result on mount
|
||||
|
||||
Add this useEffect after the hook declarations:
|
||||
|
||||
```typescript
|
||||
// Record game result once when entering results phase
|
||||
useEffect(() => {
|
||||
// Only record if we haven't already
|
||||
if (state.phase === 'results' && !state.recorded && !isRecording) {
|
||||
const gameTime = state.gameEndTime && state.gameStartTime
|
||||
? state.gameEndTime - state.gameStartTime
|
||||
: 0
|
||||
|
||||
const analysis = getPerformanceAnalysis(state)
|
||||
const multiplayerResult = gameMode === 'multiplayer'
|
||||
? getMultiplayerWinner(state, activePlayers)
|
||||
: null
|
||||
|
||||
// Build GameResult
|
||||
const gameResult: GameResult = {
|
||||
gameType: state.gameType === 'abacus-numeral'
|
||||
? 'matching-abacus'
|
||||
: 'matching-complements',
|
||||
completedAt: state.gameEndTime || Date.now(),
|
||||
duration: gameTime,
|
||||
|
||||
playerResults: activePlayers.map(playerId => {
|
||||
const score = state.scores[playerId] || 0
|
||||
const won = multiplayerResult
|
||||
? multiplayerResult.winners.includes(playerId)
|
||||
: state.matchedPairs === state.totalPairs // Solo = completed
|
||||
|
||||
// In multiplayer, calculate per-player accuracy from their score
|
||||
// In single player, use overall accuracy
|
||||
const playerAccuracy = gameMode === 'multiplayer'
|
||||
? score / state.totalPairs // Their score as fraction of total pairs
|
||||
: analysis.statistics.accuracy / 100 // Convert percentage to 0-1
|
||||
|
||||
return {
|
||||
playerId,
|
||||
won,
|
||||
score,
|
||||
accuracy: playerAccuracy,
|
||||
completionTime: gameTime,
|
||||
metrics: {
|
||||
moves: state.moves,
|
||||
matchedPairs: state.matchedPairs,
|
||||
difficulty: state.difficulty,
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
metadata: {
|
||||
gameType: state.gameType,
|
||||
difficulty: state.difficulty,
|
||||
grade: analysis.grade,
|
||||
starRating: analysis.starRating,
|
||||
}
|
||||
}
|
||||
|
||||
// Record to database
|
||||
recordGame(gameResult, {
|
||||
onSuccess: (updates) => {
|
||||
console.log('✅ Stats recorded:', updates)
|
||||
// Mark as recorded to prevent duplicate saves
|
||||
// Note: This assumes Provider has a way to update state.recorded
|
||||
// We'll need to add an action for this
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ Failed to record stats:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [state.phase, state.recorded, isRecording, /* ... deps */])
|
||||
```
|
||||
|
||||
### Step 5: Add loading state UI (optional)
|
||||
|
||||
Show a subtle loading indicator while recording:
|
||||
|
||||
```typescript
|
||||
// At the top of the return statement in ResultsPhase
|
||||
|
||||
if (isRecording) {
|
||||
return (
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
})}>
|
||||
<p>Saving results...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Or keep it subtle and just disable buttons:
|
||||
|
||||
```typescript
|
||||
// On the "Play Again" button
|
||||
<button
|
||||
disabled={isRecording}
|
||||
className={css({
|
||||
// ... styles ...
|
||||
opacity: isRecording ? 0.5 : 1,
|
||||
cursor: isRecording ? 'not-allowed' : 'pointer',
|
||||
})}
|
||||
onClick={resetGame}
|
||||
>
|
||||
{isRecording ? '💾 Saving...' : '🎮 Play Again'}
|
||||
</button>
|
||||
```
|
||||
|
||||
## Provider Changes Needed
|
||||
|
||||
The Provider needs an action to mark the game as recorded:
|
||||
|
||||
```typescript
|
||||
// src/arcade-games/matching/Provider.tsx
|
||||
|
||||
// Add to the context type
|
||||
export interface MatchingContextType {
|
||||
// ... existing ...
|
||||
markAsRecorded: () => void // ← ADD THIS
|
||||
}
|
||||
|
||||
// Add to the reducer or state update logic
|
||||
const markAsRecorded = useCallback(() => {
|
||||
setState(prev => ({ ...prev, recorded: true }))
|
||||
}, [])
|
||||
|
||||
// Add to the context value
|
||||
const contextValue: MatchingContextType = {
|
||||
// ... existing ...
|
||||
markAsRecorded,
|
||||
}
|
||||
```
|
||||
|
||||
Then in ResultsPhase useEffect:
|
||||
|
||||
```typescript
|
||||
onSuccess: (updates) => {
|
||||
console.log('✅ Stats recorded:', updates)
|
||||
markAsRecorded() // ← Use this instead
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Solo Game
|
||||
- [ ] Play a game to completion
|
||||
- [ ] Check console for "✅ Stats recorded"
|
||||
- [ ] Refresh page
|
||||
- [ ] Go to `/games` page
|
||||
- [ ] Verify player's gamesPlayed incremented
|
||||
- [ ] Verify player's totalWins incremented (if completed)
|
||||
|
||||
### Multiplayer Game
|
||||
- [ ] Activate 2+ players
|
||||
- [ ] Play a game to completion
|
||||
- [ ] Check console for stats for ALL players
|
||||
- [ ] Go to `/games` page
|
||||
- [ ] Verify each player's stats updated independently
|
||||
- [ ] Winner should have +1 win
|
||||
- [ ] All players should have +1 games played
|
||||
|
||||
### Edge Cases
|
||||
- [ ] Incomplete game (exit early) - should NOT record
|
||||
- [ ] Play again from results - should NOT duplicate record
|
||||
- [ ] Network error during save - should show error, not mark as recorded
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: Stats recorded multiple times
|
||||
**Cause**: useEffect dependency array missing or incorrect
|
||||
**Fix**: Ensure `state.recorded` is in deps and checked in condition
|
||||
|
||||
### Issue: Can't read property 'id' of undefined
|
||||
**Cause**: Player not found in playerMap
|
||||
**Fix**: Add null checks when mapping activePlayers
|
||||
|
||||
### Issue: Accuracy is always 100% or 0%
|
||||
**Cause**: Wrong calculation or unit (percentage vs decimal)
|
||||
**Fix**: Ensure accuracy is 0.0 - 1.0, not 0-100
|
||||
|
||||
### Issue: Single player never "wins"
|
||||
**Cause**: Wrong win condition for solo mode
|
||||
**Fix**: Solo player wins if they complete all pairs (`state.matchedPairs === state.totalPairs`)
|
||||
|
||||
## Next Steps After Integration
|
||||
|
||||
1. ✅ Verify stats save correctly
|
||||
2. ✅ Update `/games` page to fetch and display per-player stats
|
||||
3. ✅ Test with different game modes and difficulties
|
||||
4. 🔄 Repeat this pattern for other arcade games
|
||||
5. 📊 Add stats visualization/charts (future)
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for implementation
|
||||
**Blocked by**:
|
||||
- Database schema (player_stats table)
|
||||
- API endpoints (/api/player-stats/record-game)
|
||||
- React hooks (useRecordGameResult)
|
||||
@@ -1,594 +0,0 @@
|
||||
# Per-Player Stats Architecture & Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the architecture for tracking game statistics per-player (not per-user). Each local player profile will maintain their own game history, wins, losses, and performance metrics. We'll build a universal framework that any arcade game can use to record results.
|
||||
|
||||
**Starting point**: Matching/Memory Lightning game
|
||||
|
||||
## Current State Problems
|
||||
|
||||
1. ❌ Global `user_stats` table exists but games never update it
|
||||
2. ❌ `/games` page shows same global stats for all players
|
||||
3. ❌ No framework for games to save results
|
||||
4. ❌ Players table has no stats fields
|
||||
|
||||
## Architecture Design
|
||||
|
||||
### 1. Database Schema
|
||||
|
||||
#### New Table: `player_stats`
|
||||
|
||||
```sql
|
||||
CREATE TABLE player_stats (
|
||||
player_id TEXT PRIMARY KEY REFERENCES players(id) ON DELETE CASCADE,
|
||||
|
||||
-- Aggregate stats
|
||||
games_played INTEGER NOT NULL DEFAULT 0,
|
||||
total_wins INTEGER NOT NULL DEFAULT 0,
|
||||
total_losses INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Performance metrics
|
||||
best_time INTEGER, -- Best completion time (ms)
|
||||
highest_accuracy REAL NOT NULL DEFAULT 0, -- 0.0 - 1.0
|
||||
|
||||
-- Game preferences
|
||||
favorite_game_type TEXT, -- Most played game
|
||||
|
||||
-- Per-game stats (JSON)
|
||||
game_stats TEXT NOT NULL DEFAULT '{}', -- { "matching": { wins: 5, played: 10 }, ... }
|
||||
|
||||
-- Timestamps
|
||||
last_played_at INTEGER, -- timestamp
|
||||
created_at INTEGER NOT NULL, -- timestamp
|
||||
updated_at INTEGER NOT NULL -- timestamp
|
||||
);
|
||||
|
||||
CREATE INDEX player_stats_last_played_idx ON player_stats(last_played_at);
|
||||
```
|
||||
|
||||
#### Per-Game Stats Structure (JSON)
|
||||
|
||||
```typescript
|
||||
type PerGameStats = {
|
||||
[gameName: string]: {
|
||||
gamesPlayed: number
|
||||
wins: number
|
||||
losses: number
|
||||
bestTime: number | null
|
||||
highestAccuracy: number
|
||||
averageScore: number
|
||||
lastPlayed: number // timestamp
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Keep `user_stats`?
|
||||
|
||||
**Decision**: Deprecate `user_stats` table. All stats are now per-player.
|
||||
|
||||
**Reasoning**:
|
||||
- Users can have multiple players
|
||||
- Aggregate "user level" stats can be computed by summing player stats
|
||||
- Simpler mental model: players compete, players have stats
|
||||
- `/games` page displays players, so showing player stats makes sense
|
||||
|
||||
### 2. Universal Game Result Types
|
||||
|
||||
**Analysis**: Examined 5 arcade games (Matching, Complement Race, Memory Quiz, Card Sorting, Rithmomachia)
|
||||
**Key Finding**: Cooperative games need special handling - all players share win/loss!
|
||||
**See**: `.claude/GAME_STATS_COMPARISON.md` for detailed cross-game analysis
|
||||
|
||||
```typescript
|
||||
// src/lib/arcade/stats/types.ts
|
||||
|
||||
/**
|
||||
* Standard game result that all arcade games must provide
|
||||
*
|
||||
* Supports:
|
||||
* - 1-N players
|
||||
* - Competitive (individual winners)
|
||||
* - Cooperative (team wins/losses)
|
||||
* - Solo completion
|
||||
* - Head-to-head (2-player)
|
||||
*/
|
||||
export interface GameResult {
|
||||
// Game identification
|
||||
gameType: string // e.g., "matching", "complement-race", "memory-quiz"
|
||||
|
||||
// Player results (for multiplayer, array of results)
|
||||
playerResults: PlayerGameResult[]
|
||||
|
||||
// Game metadata
|
||||
completedAt: number // timestamp
|
||||
duration: number // milliseconds
|
||||
|
||||
// Optional game-specific data
|
||||
metadata?: {
|
||||
// For cooperative games (Memory Quiz, Card Sorting collaborative)
|
||||
isTeamVictory?: boolean // All players share win/loss
|
||||
|
||||
// For specific win conditions (Rithmomachia)
|
||||
winCondition?: string // e.g., "HARMONY", "POINTS", "TIMEOUT"
|
||||
|
||||
// For game modes
|
||||
gameMode?: string // e.g., "solo", "competitive", "cooperative"
|
||||
|
||||
// Extensible for other game-specific info
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlayerGameResult {
|
||||
playerId: string
|
||||
|
||||
// Outcome
|
||||
won: boolean // For cooperative: all players have same value
|
||||
placement?: number // 1st, 2nd, 3rd place (for tournaments with 3+ players)
|
||||
|
||||
// Performance
|
||||
score?: number
|
||||
accuracy?: number // 0.0 - 1.0
|
||||
completionTime?: number // milliseconds (player-specific)
|
||||
|
||||
// Game-specific metrics (stored as JSON in DB)
|
||||
metrics?: {
|
||||
// Matching
|
||||
moves?: number
|
||||
matchedPairs?: number
|
||||
difficulty?: number
|
||||
|
||||
// Complement Race
|
||||
streak?: number
|
||||
correctAnswers?: number
|
||||
totalQuestions?: number
|
||||
|
||||
// Memory Quiz
|
||||
correct?: number
|
||||
incorrect?: number
|
||||
|
||||
// Card Sorting
|
||||
exactMatches?: number
|
||||
inversions?: number
|
||||
lcsLength?: number
|
||||
|
||||
// Rithmomachia
|
||||
capturedPieces?: number
|
||||
points?: number
|
||||
|
||||
// Extensible for future games
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats update returned from API
|
||||
*/
|
||||
export interface StatsUpdate {
|
||||
playerId: string
|
||||
previousStats: PlayerStats
|
||||
newStats: PlayerStats
|
||||
changes: {
|
||||
gamesPlayed: number
|
||||
wins: number
|
||||
losses: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlayerStats {
|
||||
playerId: string
|
||||
gamesPlayed: number
|
||||
totalWins: number
|
||||
totalLosses: number
|
||||
bestTime: number | null
|
||||
highestAccuracy: number
|
||||
favoriteGameType: string | null
|
||||
gameStats: PerGameStats
|
||||
lastPlayedAt: number | null
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
### 3. API Endpoints
|
||||
|
||||
#### POST `/api/player-stats/record-game`
|
||||
|
||||
Records a game result and updates player stats.
|
||||
|
||||
**Request:**
|
||||
```typescript
|
||||
{
|
||||
gameResult: GameResult
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
success: true,
|
||||
updates: StatsUpdate[] // One per player
|
||||
}
|
||||
```
|
||||
|
||||
**Logic:**
|
||||
1. Validate game result structure
|
||||
2. For each player result:
|
||||
- Fetch or create player_stats record
|
||||
- Increment games_played
|
||||
- Increment wins/losses based on outcome
|
||||
- **Special case**: If `metadata.isTeamVictory === true`, all players share win/loss
|
||||
- Cooperative games: all win or all lose together
|
||||
- Competitive games: individual outcomes
|
||||
- Update best_time if improved
|
||||
- Update highest_accuracy if improved
|
||||
- Update game-specific stats in JSON
|
||||
- Update favorite_game_type based on most played
|
||||
- Set last_played_at
|
||||
3. Return updates for all players
|
||||
|
||||
**Example pseudo-code**:
|
||||
```typescript
|
||||
for (const playerResult of gameResult.playerResults) {
|
||||
const stats = await getPlayerStats(playerResult.playerId)
|
||||
|
||||
stats.gamesPlayed++
|
||||
|
||||
// Handle cooperative games specially
|
||||
if (gameResult.metadata?.isTeamVictory !== undefined) {
|
||||
// Cooperative: all players share outcome
|
||||
if (playerResult.won) {
|
||||
stats.totalWins++
|
||||
} else {
|
||||
stats.totalLosses++
|
||||
}
|
||||
} else {
|
||||
// Competitive/Solo: individual outcome
|
||||
if (playerResult.won) {
|
||||
stats.totalWins++
|
||||
} else {
|
||||
stats.totalLosses++
|
||||
}
|
||||
}
|
||||
|
||||
// ... rest of stats update
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/player-stats/:playerId`
|
||||
|
||||
Fetch stats for a specific player.
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
stats: PlayerStats
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/player-stats`
|
||||
|
||||
Fetch stats for all current user's players.
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
playerStats: PlayerStats[]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. React Hooks
|
||||
|
||||
#### `useRecordGameResult()`
|
||||
|
||||
Main hook that games use to record results.
|
||||
|
||||
```typescript
|
||||
// src/hooks/useRecordGameResult.ts
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import type { GameResult, StatsUpdate } from '@/lib/arcade/stats/types'
|
||||
|
||||
export function useRecordGameResult() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (gameResult: GameResult): Promise<StatsUpdate[]> => {
|
||||
const res = await fetch('/api/player-stats/record-game', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gameResult }),
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error('Failed to record game result')
|
||||
|
||||
const data = await res.json()
|
||||
return data.updates
|
||||
},
|
||||
|
||||
onSuccess: (updates) => {
|
||||
// Invalidate player stats queries to trigger refetch
|
||||
queryClient.invalidateQueries({ queryKey: ['player-stats'] })
|
||||
|
||||
// Show success feedback (optional)
|
||||
console.log('✅ Game result recorded:', updates)
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
console.error('❌ Failed to record game result:', error)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### `usePlayerStats(playerId?)`
|
||||
|
||||
Fetch stats for a player (or all players if no ID).
|
||||
|
||||
```typescript
|
||||
// src/hooks/usePlayerStats.ts
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { PlayerStats } from '@/lib/arcade/stats/types'
|
||||
|
||||
export function usePlayerStats(playerId?: string) {
|
||||
return useQuery({
|
||||
queryKey: playerId ? ['player-stats', playerId] : ['player-stats'],
|
||||
queryFn: async (): Promise<PlayerStats | PlayerStats[]> => {
|
||||
const url = playerId
|
||||
? `/api/player-stats/${playerId}`
|
||||
: '/api/player-stats'
|
||||
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) throw new Error('Failed to fetch player stats')
|
||||
|
||||
const data = await res.json()
|
||||
return playerId ? data.stats : data.playerStats
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Game Integration Pattern
|
||||
|
||||
Every arcade game should follow this pattern when completing:
|
||||
|
||||
```typescript
|
||||
// In results phase component (e.g., ResultsPhase.tsx)
|
||||
|
||||
import { useRecordGameResult } from '@/hooks/useRecordGameResult'
|
||||
import type { GameResult } from '@/lib/arcade/stats/types'
|
||||
|
||||
export function ResultsPhase() {
|
||||
const { state, activePlayers } = useGameContext()
|
||||
const { mutate: recordGame, isPending } = useRecordGameResult()
|
||||
|
||||
// Record game result on mount (once)
|
||||
useEffect(() => {
|
||||
if (state.phase === 'results' && !state.recorded) {
|
||||
const gameResult: GameResult = {
|
||||
gameType: 'matching',
|
||||
completedAt: Date.now(),
|
||||
duration: state.gameEndTime - state.gameStartTime,
|
||||
playerResults: activePlayers.map(player => ({
|
||||
playerId: player.id,
|
||||
won: player.id === winnerId,
|
||||
score: player.matchCount,
|
||||
accuracy: player.matchCount / state.totalPairs,
|
||||
completionTime: player.completionTime,
|
||||
})),
|
||||
}
|
||||
|
||||
recordGame(gameResult, {
|
||||
onSuccess: () => {
|
||||
// Mark as recorded to prevent duplicates
|
||||
setState({ recorded: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [state.phase, state.recorded])
|
||||
|
||||
// Show loading state while recording
|
||||
if (isPending) {
|
||||
return <div>Saving results...</div>
|
||||
}
|
||||
|
||||
// Show results UI
|
||||
return <div>...</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Foundation (Database & API)
|
||||
|
||||
1. **Create database schema**
|
||||
- File: `src/db/schema/player-stats.ts`
|
||||
- Define `player_stats` table with Drizzle ORM
|
||||
- Add type exports
|
||||
|
||||
2. **Generate migration**
|
||||
```bash
|
||||
npx drizzle-kit generate:sqlite
|
||||
```
|
||||
|
||||
3. **Create type definitions**
|
||||
- File: `src/lib/arcade/stats/types.ts`
|
||||
- Define `GameResult`, `PlayerGameResult`, `StatsUpdate`, `PlayerStats`
|
||||
|
||||
4. **Build API endpoint**
|
||||
- File: `src/app/api/player-stats/record-game/route.ts`
|
||||
- Implement POST handler with validation
|
||||
- Handle per-player stat updates
|
||||
- Transaction safety
|
||||
|
||||
5. **Build query endpoints**
|
||||
- File: `src/app/api/player-stats/route.ts` (GET all)
|
||||
- File: `src/app/api/player-stats/[playerId]/route.ts` (GET one)
|
||||
|
||||
### Phase 2: React Hooks & Integration
|
||||
|
||||
6. **Create React hooks**
|
||||
- File: `src/hooks/useRecordGameResult.ts`
|
||||
- File: `src/hooks/usePlayerStats.ts`
|
||||
|
||||
7. **Update GameModeContext**
|
||||
- Expose helper to get player stats map
|
||||
- Integrate with usePlayerStats hook
|
||||
|
||||
### Phase 3: Matching Game Integration
|
||||
|
||||
8. **Analyze matching game completion flow**
|
||||
- Find where game completes
|
||||
- Identify winner calculation
|
||||
- Map state to GameResult format
|
||||
|
||||
9. **Integrate stats recording**
|
||||
- Add useRecordGameResult to ResultsPhase
|
||||
- Build GameResult from game state
|
||||
- Handle recording state to prevent duplicates
|
||||
|
||||
10. **Test matching game stats**
|
||||
- Play solo game, verify stats update
|
||||
- Play multiplayer game, verify all players update
|
||||
- Check accuracy calculations
|
||||
- Check time tracking
|
||||
|
||||
### Phase 4: UI Updates
|
||||
|
||||
11. **Update /games page**
|
||||
- Fetch per-player stats with usePlayerStats
|
||||
- Display correct stats for each player card
|
||||
- Remove dependency on global user profile
|
||||
|
||||
12. **Add stats visualization**
|
||||
- Per-game breakdown
|
||||
- Win/loss ratio
|
||||
- Performance trends
|
||||
|
||||
### Phase 5: Documentation & Rollout
|
||||
|
||||
13. **Document integration pattern**
|
||||
- Create guide for adding stats to other games
|
||||
- Code examples
|
||||
- Common pitfalls
|
||||
|
||||
14. **Roll out to other games**
|
||||
- Complement Race
|
||||
- Memory Quiz
|
||||
- Card Sorting
|
||||
- (Future games)
|
||||
|
||||
## Data Migration Strategy
|
||||
|
||||
### Handling Existing `user_stats`
|
||||
|
||||
**Option A: Drop the table**
|
||||
- Simple, clean break
|
||||
- No historical data
|
||||
|
||||
**Option B: Migrate to player stats**
|
||||
- For each user with stats, assign to their first/active player
|
||||
- More complex but preserves history
|
||||
|
||||
**Recommendation**: Option A (drop it) since:
|
||||
- Very new feature, unlikely much data exists
|
||||
- Cleaner architecture
|
||||
- Users can rebuild stats by playing
|
||||
|
||||
### Migration SQL
|
||||
|
||||
```sql
|
||||
-- Drop old user_stats table
|
||||
DROP TABLE IF EXISTS user_stats;
|
||||
|
||||
-- Create new player_stats table
|
||||
-- (Drizzle migration will handle this)
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `GameResult` validation
|
||||
- Stats calculation logic
|
||||
- JSON merge for per-game stats
|
||||
- Favorite game detection
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- API endpoint: record game, verify DB update
|
||||
- API endpoint: fetch stats, verify response
|
||||
- React hook: record game, verify cache invalidation
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Play matching game solo, check stats on /games page
|
||||
- Play matching game multiplayer, verify each player's stats
|
||||
- Verify stats persist across sessions
|
||||
|
||||
## Success Criteria
|
||||
|
||||
✅ Player stats save correctly after game completion
|
||||
✅ Each player maintains separate stats
|
||||
✅ /games page displays correct per-player stats
|
||||
✅ Stats survive page refresh
|
||||
✅ Multiplayer games update all participants
|
||||
✅ Framework is reusable for other games
|
||||
✅ No duplicate recordings
|
||||
✅ Performance acceptable (< 200ms to record)
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Leaderboards?** - Future consideration, need global rankings
|
||||
2. **Historical games?** - Store individual game records or just aggregates?
|
||||
3. **Stats reset?** - Should users be able to reset player stats?
|
||||
4. **Achievements?** - Track milestones? (100 games, 50 wins, etc.)
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── db/
|
||||
│ └── schema/
|
||||
│ └── player-stats.ts # NEW: Drizzle schema
|
||||
├── lib/
|
||||
│ └── arcade/
|
||||
│ └── stats/
|
||||
│ ├── types.ts # NEW: Type definitions
|
||||
│ └── utils.ts # NEW: Helper functions
|
||||
├── hooks/
|
||||
│ ├── useRecordGameResult.ts # NEW: Record game hook
|
||||
│ └── usePlayerStats.ts # NEW: Fetch stats hook
|
||||
├── app/
|
||||
│ └── api/
|
||||
│ └── player-stats/
|
||||
│ ├── route.ts # NEW: GET all
|
||||
│ ├── record-game/
|
||||
│ │ └── route.ts # NEW: POST record
|
||||
│ └── [playerId]/
|
||||
│ └── route.ts # NEW: GET one
|
||||
└── arcade-games/
|
||||
└── matching/
|
||||
└── components/
|
||||
└── ResultsPhase.tsx # MODIFY: Add stats recording
|
||||
|
||||
.claude/
|
||||
└── PER_PLAYER_STATS_ARCHITECTURE.md # THIS FILE
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review this plan with user
|
||||
2. Create database schema and types
|
||||
3. Build API endpoints
|
||||
4. Create React hooks
|
||||
5. Integrate with matching game
|
||||
6. Test thoroughly
|
||||
7. Roll out to other games
|
||||
|
||||
---
|
||||
|
||||
**Document Status**: Draft for review
|
||||
**Last Updated**: 2025-01-03
|
||||
**Owner**: Claude Code
|
||||
@@ -1,943 +0,0 @@
|
||||
# Platform Integration Roadmap
|
||||
|
||||
**Project:** Soroban Abacus Flashcards - Arcade Games Platform
|
||||
**Focus Areas:** Educational Platform SSO & Game Distribution Channels
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2025-11-02
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This roadmap outlines integration strategies for four major platform categories to expand reach and player acquisition:
|
||||
|
||||
1. **Google Classroom Integration** - Direct access to teachers and students
|
||||
2. **Clever/ClassLink SSO** - K-12 institution single sign-on (massive reach)
|
||||
3. **Game Distribution Portals** - CrazyGames, Poki, Kongregate (casual player discovery)
|
||||
4. **Steam Distribution** - Educational game marketplace with social features
|
||||
|
||||
**Estimated Total Timeline:** 6-9 months for full implementation
|
||||
**Estimated Total Cost:** $600-1,500 (app fees + hosting)
|
||||
**Expected Impact:** 10x-100x increase in player discovery reach
|
||||
|
||||
---
|
||||
|
||||
## Platform #1: Google Classroom Integration
|
||||
|
||||
### Overview
|
||||
|
||||
Google Classroom API enables teachers to:
|
||||
- Import game rooms directly into their classes
|
||||
- Auto-create student accounts with SSO
|
||||
- Track student progress and scores
|
||||
- Assign abacus practice as homework
|
||||
|
||||
**Target Audience:** 150+ million students and teachers using Google Classroom globally
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
**Prerequisites:**
|
||||
- Google Cloud Platform (GCP) project
|
||||
- OAuth 2.0 implementation
|
||||
- Google Classroom API access
|
||||
- HTTPS endpoints
|
||||
|
||||
**API Capabilities Needed:**
|
||||
- Courses API (read class rosters)
|
||||
- Coursework API (create assignments)
|
||||
- Students API (manage student accounts)
|
||||
- Submissions API (track game completion)
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Basic OAuth & SSO (2-3 weeks)
|
||||
**Tasks:**
|
||||
1. Set up GCP project and enable Classroom API
|
||||
2. Implement OAuth 2.0 "Sign in with Google" flow
|
||||
3. Add Classroom scope permissions (`classroom.courses.readonly`, `classroom.rosters.readonly`)
|
||||
4. Create user account mapping (Google ID → Internal User ID)
|
||||
5. Test with sandbox Google Workspace account
|
||||
|
||||
**Deliverables:**
|
||||
- "Sign in with Google" button on login page
|
||||
- Auto-account creation for Google users
|
||||
- Basic profile sync (name, email, photo)
|
||||
|
||||
**Effort:** 40-60 hours
|
||||
**Cost:** $0 (Google API is free for educational use)
|
||||
|
||||
#### Phase 2: Class Import & Roster Sync (3-4 weeks)
|
||||
**Tasks:**
|
||||
1. Build UI for teachers to import Classroom rosters
|
||||
2. Implement Courses API integration (list teacher's classes)
|
||||
3. Create room auto-provisioning from class data
|
||||
4. Set up automatic roster synchronization (daily sync)
|
||||
5. Handle student account creation/deactivation
|
||||
6. Add class management dashboard for teachers
|
||||
|
||||
**Deliverables:**
|
||||
- "Import from Google Classroom" feature
|
||||
- Teacher dashboard showing all imported classes
|
||||
- Automatic student account provisioning
|
||||
- Daily roster sync (new students, removed students)
|
||||
|
||||
**Effort:** 80-100 hours
|
||||
**Cost:** $0
|
||||
|
||||
#### Phase 3: Assignment Integration (3-4 weeks)
|
||||
**Tasks:**
|
||||
1. Build "Assign to Classroom" feature for game rooms
|
||||
2. Implement Coursework API integration
|
||||
3. Create assignment templates for each game type
|
||||
4. Build grade sync (game scores → Classroom grades)
|
||||
5. Add completion tracking and submission API integration
|
||||
6. Create teacher analytics dashboard
|
||||
|
||||
**Deliverables:**
|
||||
- Teachers can push game assignments to Classroom
|
||||
- Students see assignments in Classroom feed
|
||||
- Automatic grade passback on game completion
|
||||
- Teacher analytics (who played, scores, time spent)
|
||||
|
||||
**Effort:** 100-120 hours
|
||||
**Cost:** $0
|
||||
|
||||
#### Phase 4: Deep Integration & Polish (2-3 weeks)
|
||||
**Tasks:**
|
||||
1. Add "Share Turn" notifications (via Classroom API announcements)
|
||||
2. Implement progress tracking dashboard
|
||||
3. Create teacher resource library (lesson plans, tutorials)
|
||||
4. Add student portfolio (history of games played)
|
||||
5. Build reporting exports (CSV, PDF)
|
||||
6. Classroom API webhook integration for real-time updates
|
||||
|
||||
**Deliverables:**
|
||||
- Comprehensive teacher dashboard
|
||||
- Student progress portfolios
|
||||
- Automated reporting
|
||||
- Real-time roster updates
|
||||
|
||||
**Effort:** 60-80 hours
|
||||
**Cost:** $0
|
||||
|
||||
### Total Timeline: 10-14 weeks
|
||||
### Total Effort: 280-360 hours
|
||||
### Total Cost: $0
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Existing user authentication system (✓ NextAuth in place)
|
||||
- Room management system (✓ exists)
|
||||
- Score/progress tracking (✓ exists per game)
|
||||
- Email notification system (partially implemented)
|
||||
|
||||
### Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| OAuth complexity | Medium | Medium | Use official Google client libraries |
|
||||
| API rate limits | Low | Low | Implement caching and batch requests |
|
||||
| Grade sync errors | High | Medium | Add retry logic and error notifications |
|
||||
| Teacher adoption | High | High | Create detailed onboarding videos |
|
||||
| Privacy compliance (COPPA) | Critical | Medium | Implement parent consent workflows |
|
||||
|
||||
### Success Metrics
|
||||
|
||||
- **Adoption:** 100+ teachers in first 3 months
|
||||
- **Usage:** 50+ classes imported
|
||||
- **Retention:** 70%+ of teachers assign games monthly
|
||||
- **Referrals:** 20% teacher-to-teacher referrals
|
||||
|
||||
---
|
||||
|
||||
## Platform #2: Clever & ClassLink SSO
|
||||
|
||||
### Overview
|
||||
|
||||
**Clever:** SSO platform serving 75% of U.S. K-12 schools (13,000+ districts)
|
||||
**ClassLink:** SSO platform serving 20+ million students globally
|
||||
|
||||
Both provide instant access to school rosters without manual setup.
|
||||
|
||||
**Key Benefits:**
|
||||
- Zero teacher setup (IT manages access)
|
||||
- Automatic roster sync
|
||||
- Instant student login (no passwords)
|
||||
- District-wide deployment capability
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
**Clever Requirements:**
|
||||
- OAuth 2.0 implementation
|
||||
- REST API integration
|
||||
- HTTPS endpoints
|
||||
- District SSO certification
|
||||
|
||||
**ClassLink Requirements:**
|
||||
- SAML 2.0 support (or OAuth/OpenID Connect)
|
||||
- OneRoster API integration (for roster sync)
|
||||
- ClassLink Management Console access
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Clever District SSO (4-5 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Register as Clever developer (dev.clever.com)
|
||||
2. Implement OAuth 2.0 authorization grant flow
|
||||
3. Add "Log in with Clever" button
|
||||
4. Integrate Clever Data API v3.1
|
||||
- Districts API
|
||||
- Schools API
|
||||
- Students/Teachers API
|
||||
- Sections API (classes)
|
||||
5. Build user account mapping (Clever ID → Internal ID)
|
||||
6. Create development environment with sandbox district
|
||||
7. Implement district admin dashboard
|
||||
8. Apply for District SSO certification
|
||||
|
||||
**API Scopes Required:**
|
||||
- `read:district_admins_basic`
|
||||
- `read:school_admins_basic`
|
||||
- `read:students_basic`
|
||||
- `read:teachers_basic`
|
||||
- `read:user_id`
|
||||
- `read:sis` (for full roster data)
|
||||
|
||||
**Deliverables:**
|
||||
- "Log in with Clever" SSO
|
||||
- Automatic account creation
|
||||
- District/school/class hierarchy sync
|
||||
- Certified for live district connections
|
||||
|
||||
**Effort:** 100-120 hours
|
||||
**Cost:** $0 (Clever is free for developers)
|
||||
|
||||
#### Phase 2: ClassLink SAML Integration (3-4 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Set up SAML 2.0 service provider
|
||||
2. Register with ClassLink SSO Library
|
||||
3. Implement SAML authentication flow
|
||||
4. Add "Log in with ClassLink" button
|
||||
5. Integrate OneRoster API for roster sync
|
||||
6. Build automated roster update system
|
||||
7. Create ClassLink admin dashboard
|
||||
8. Submit to ClassLink SSO Library (6,000+ app directory)
|
||||
|
||||
**Deliverables:**
|
||||
- "Log in with ClassLink" SSO
|
||||
- OneRoster-based roster sync
|
||||
- Listed in ClassLink SSO Library
|
||||
|
||||
**Effort:** 80-100 hours
|
||||
**Cost:** $0
|
||||
|
||||
#### Phase 3: Advanced Features (2-3 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Implement shared device support (session override)
|
||||
2. Add UTF-8 character support for names
|
||||
3. Build district admin analytics dashboard
|
||||
4. Create automated onboarding emails for admins
|
||||
5. Add Clever/ClassLink badge to marketing site
|
||||
6. Build compliance documentation (FERPA, COPPA)
|
||||
|
||||
**Deliverables:**
|
||||
- Production-ready SSO for both platforms
|
||||
- District admin tools
|
||||
- Compliance documentation
|
||||
|
||||
**Effort:** 60-80 hours
|
||||
**Cost:** $0
|
||||
|
||||
### Total Timeline: 9-12 weeks
|
||||
### Total Effort: 240-300 hours
|
||||
### Total Cost: $0
|
||||
|
||||
### Dependencies
|
||||
|
||||
- SAML library (e.g., `passport-saml` for Node.js)
|
||||
- OAuth 2.0 implementation (can reuse from Google Classroom)
|
||||
- Secure session management
|
||||
- Database schema for district/school/class hierarchy
|
||||
|
||||
### Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| Certification delays | High | Medium | Start certification process early |
|
||||
| SAML complexity | Medium | Medium | Use established libraries (passport-saml) |
|
||||
| District adoption | High | Medium | Partner with early adopter districts |
|
||||
| Privacy regulations | Critical | Medium | Legal review of terms/privacy policy |
|
||||
| Competing apps | Medium | High | Emphasize unique abacus/math focus |
|
||||
|
||||
### Success Metrics
|
||||
|
||||
- **Listings:** Published in both app directories
|
||||
- **Certification:** Clever District SSO certified
|
||||
- **Districts:** 10+ districts in first 6 months
|
||||
- **Students:** 5,000+ student logins in first year
|
||||
- **Retention:** 60%+ district renewal rate
|
||||
|
||||
---
|
||||
|
||||
## Platform #3: Game Distribution Portals
|
||||
|
||||
### Overview
|
||||
|
||||
Three major browser game portals with complementary audiences:
|
||||
|
||||
1. **CrazyGames:** Open platform, 35M+ monthly users, SDK monetization
|
||||
2. **Poki:** Curated platform, 50M+ monthly users, 50/50 revenue share, web exclusivity
|
||||
3. **Kongregate:** Legacy platform, 20M+ monthly users, now requires pre-approval
|
||||
|
||||
**Target Audience:** Casual gamers discovering educational games through play
|
||||
|
||||
### Platform Comparison
|
||||
|
||||
| Feature | CrazyGames | Poki | Kongregate |
|
||||
|---------|------------|------|------------|
|
||||
| **Selectivity** | Moderate | High (curated) | High (email approval) |
|
||||
| **File Size** | Strict limits | <8MB initial | ~1100x700px max |
|
||||
| **Exclusivity** | None | Web exclusive | None |
|
||||
| **Revenue** | SDK ads | 50/50 split | Ads + Kreds |
|
||||
| **SDK Required** | Yes | Yes | Yes |
|
||||
| **Approval Time** | 1-2 weeks | 2-4 weeks | Varies |
|
||||
|
||||
### Technical Requirements (Common)
|
||||
|
||||
**All Platforms:**
|
||||
- HTML5/WebGL build
|
||||
- Save system (localStorage/cloud)
|
||||
- Ad integration (SDK provided)
|
||||
- Mobile responsive design
|
||||
- HTTPS hosting
|
||||
- No external monetization UI
|
||||
|
||||
**CrazyGames Specific:**
|
||||
- CrazyGames SDK integration
|
||||
- File size optimization
|
||||
- Ad placements (banner, interstitial, rewarded)
|
||||
- Leaderboard integration
|
||||
|
||||
**Poki Specific:**
|
||||
- Poki SDK integration
|
||||
- <8MB initial download (critical!)
|
||||
- Auto-detect mobile devices
|
||||
- Remove any IAP UI elements
|
||||
- Web exclusivity agreement
|
||||
|
||||
**Kongregate Specific:**
|
||||
- Pre-approval via BD@kongregate.com
|
||||
- Kongregate API integration
|
||||
- Badge system integration
|
||||
- Kreds (virtual currency) support optional
|
||||
- "initialized" stat tracking
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Game Optimization & SDK Prep (3-4 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Audit current build sizes (check all games)
|
||||
2. Implement code splitting and lazy loading
|
||||
3. Optimize assets (image compression, audio formats)
|
||||
4. Create lightweight "portal build" configuration
|
||||
5. Build asset CDN strategy
|
||||
6. Implement progress save system (if not present)
|
||||
7. Add mobile detection and responsive layouts
|
||||
8. Create build pipeline for portal releases
|
||||
|
||||
**Target Build Sizes:**
|
||||
- Initial load: <5MB (to meet Poki's <8MB requirement)
|
||||
- Full game: <20MB
|
||||
- Individual game assets: lazy loaded
|
||||
|
||||
**Deliverables:**
|
||||
- Optimized builds for each game
|
||||
- Build pipeline for portal releases
|
||||
- Mobile-responsive UI for all games
|
||||
|
||||
**Effort:** 80-100 hours
|
||||
**Cost:** $0
|
||||
|
||||
#### Phase 2: CrazyGames Integration (2-3 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Register at CrazyGames developer portal
|
||||
2. Integrate CrazyGames SDK
|
||||
- Ad placements (banner, interstitial, rewarded)
|
||||
- Game analytics
|
||||
- User progression tracking
|
||||
3. Submit best-performing games:
|
||||
- Matching Pairs Battle (most casual-friendly)
|
||||
- Complement Race (fast-paced)
|
||||
- Card Sorting (quick sessions)
|
||||
4. Create game pages with descriptions/screenshots
|
||||
5. Pass technical review and QA
|
||||
6. Launch and monitor metrics
|
||||
|
||||
**Deliverables:**
|
||||
- 3 games live on CrazyGames
|
||||
- SDK integration complete
|
||||
- Ad monetization active
|
||||
|
||||
**Effort:** 60-80 hours
|
||||
**Cost:** $0
|
||||
|
||||
#### Phase 3: Poki Integration (3-4 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Submit application via Poki game submission form
|
||||
2. Wait for curator review (2-4 weeks)
|
||||
3. If approved, integrate Poki SDK
|
||||
- Ad integration (Poki manages monetization)
|
||||
- Analytics tracking
|
||||
- Commerce API (if applicable)
|
||||
4. Agree to web exclusivity terms
|
||||
5. Optimize games to <8MB initial load (critical!)
|
||||
6. Remove any competing monetization UI
|
||||
7. Submit games for final review
|
||||
8. Launch on Poki
|
||||
|
||||
**Deliverables:**
|
||||
- 2-3 games live on Poki (start with best performers)
|
||||
- Poki SDK integration
|
||||
- Revenue share active (50/50 split)
|
||||
|
||||
**Effort:** 80-100 hours
|
||||
**Cost:** $0
|
||||
**Note:** Approval not guaranteed - Poki is highly selective
|
||||
|
||||
#### Phase 4: Kongregate Pre-Approval (2-3 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Email BD@kongregate.com with game portfolio
|
||||
2. Prepare pitch deck:
|
||||
- Game trailers/screenshots
|
||||
- Unique value proposition (abacus education + multiplayer)
|
||||
- Target audience data
|
||||
- Existing player metrics
|
||||
3. Wait for response (varies)
|
||||
4. If approved, integrate Kongregate API
|
||||
- Stats and achievements (badges)
|
||||
- Optional: Kreds integration
|
||||
5. Submit games for review
|
||||
6. Launch on Kongregate
|
||||
|
||||
**Deliverables:**
|
||||
- Pre-approval from Kongregate
|
||||
- 1-2 games live on platform
|
||||
- Badge achievements implemented
|
||||
|
||||
**Effort:** 60-80 hours
|
||||
**Cost:** $0
|
||||
**Note:** Approval required before development
|
||||
|
||||
### Total Timeline: 10-14 weeks
|
||||
### Total Effort: 280-360 hours
|
||||
### Total Cost: $0
|
||||
|
||||
### Game Prioritization for Submission
|
||||
|
||||
**Tier 1 (Submit first):**
|
||||
1. **Matching Pairs Battle** - Most casual-friendly, clear mechanics
|
||||
2. **Complement Race** - Fast-paced, competitive, good for short sessions
|
||||
|
||||
**Tier 2 (Submit if Tier 1 succeeds):**
|
||||
3. **Card Sorting** - Simple, quick gameplay
|
||||
4. **Memory Quiz** - Educational but approachable
|
||||
|
||||
**Tier 3 (Hold back):**
|
||||
5. **Rithmomachia** - Too complex for casual portals, better for Steam
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Current games must be playable without login (guest mode)
|
||||
- Mobile responsive design
|
||||
- Build optimization pipeline
|
||||
- CDN for asset hosting
|
||||
- Analytics integration
|
||||
|
||||
### Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| Poki rejection | Medium | High | Focus on CrazyGames first, apply with best metrics |
|
||||
| File size requirements | High | Medium | Aggressive asset optimization, code splitting |
|
||||
| Ad integration conflicts | Medium | Medium | Separate builds per platform |
|
||||
| Revenue lower than expected | Medium | High | Treat as marketing channel, not primary revenue |
|
||||
| Web exclusivity limits Steam | High | Low | Don't submit Rithmomachia to Poki |
|
||||
| Multiplayer sync issues | High | Medium | Add offline/single-player modes |
|
||||
|
||||
### Success Metrics
|
||||
|
||||
**CrazyGames:**
|
||||
- **Plays:** 10,000+ plays/month per game
|
||||
- **Retention:** 30%+ day-1 return rate
|
||||
- **Revenue:** $200+ monthly from ads
|
||||
|
||||
**Poki:**
|
||||
- **Plays:** 50,000+ plays/month per game
|
||||
- **Revenue:** $500+ monthly (50/50 split)
|
||||
- **Rating:** 4.0+ stars
|
||||
|
||||
**Kongregate:**
|
||||
- **Plays:** 5,000+ plays/month
|
||||
- **Badges:** 3+ achievements per game
|
||||
- **Revenue:** $100+ monthly
|
||||
|
||||
---
|
||||
|
||||
## Platform #4: Steam Distribution
|
||||
|
||||
### Overview
|
||||
|
||||
**Why Steam for Educational Games:**
|
||||
- 120+ million active users
|
||||
- Strong social features (friends, achievements, leaderboards)
|
||||
- Educational games perform well (Kerbal Space Program, Human Resource Machine, etc.)
|
||||
- One-time purchase model fits premium educational content
|
||||
- Community features drive multiplayer engagement
|
||||
|
||||
**Package Concept:** "Soroban Academy: Mathematical Strategy Games"
|
||||
- Bundle all games into single Steam app
|
||||
- Include exclusive Steam features (achievements, leaderboards, cloud saves)
|
||||
- Highlight Rithmomachia as flagship historical strategy game
|
||||
- Price point: $9.99-14.99
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
**Platform:**
|
||||
- Desktop application (Windows, macOS, Linux)
|
||||
- No native browser game support
|
||||
|
||||
**Packaging Options:**
|
||||
1. **Electron** (most common for web games)
|
||||
- Pros: Easy conversion, full Chromium
|
||||
- Cons: Large file size (~100-200MB), memory intensive
|
||||
|
||||
2. **Tauri** (modern alternative)
|
||||
- Pros: Lightweight (~10MB), uses system WebView
|
||||
- Cons: Newer, less Steam integration examples
|
||||
|
||||
3. **NW.js** (alternative to Electron)
|
||||
- Pros: Similar to Electron, slightly smaller
|
||||
- Cons: Less popular, smaller community
|
||||
|
||||
**Recommended:** Start with Electron (most proven path for HTML5 → Steam)
|
||||
|
||||
**Steamworks Integration:**
|
||||
- Steam Authentication API
|
||||
- Steam Achievements API
|
||||
- Steam Leaderboards API
|
||||
- Steam Cloud (save sync)
|
||||
- Steam Friends API (invite friends to games)
|
||||
- Steam Overlay support
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Desktop Packaging with Electron (4-5 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Set up Electron project
|
||||
2. Package Next.js app for Electron
|
||||
3. Configure build pipeline:
|
||||
- Windows (x64)
|
||||
- macOS (Apple Silicon + Intel)
|
||||
- Linux (x64)
|
||||
4. Handle offline mode and local Socket.io server
|
||||
5. Implement native window controls
|
||||
6. Add auto-updater support
|
||||
7. Test on all platforms
|
||||
8. Optimize bundle size (<500MB total)
|
||||
|
||||
**Deliverables:**
|
||||
- Desktop app for Windows/macOS/Linux
|
||||
- Standalone builds (no separate server needed)
|
||||
- Native app experience
|
||||
|
||||
**Effort:** 120-150 hours
|
||||
**Cost:** $0
|
||||
|
||||
#### Phase 2: Steamworks SDK Integration (3-4 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Register as Steamworks developer (pay $100 fee)
|
||||
2. Create Steam app page
|
||||
3. Integrate Steamworks SDK:
|
||||
- Install Greenworks (Electron + Steamworks bridge)
|
||||
- Implement Steam Authentication
|
||||
- Replace internal auth with Steam accounts
|
||||
4. Build achievement system:
|
||||
- Define 20-30 achievements per game
|
||||
- Integrate Steamworks Achievements API
|
||||
- Create achievement icons (64x64px)
|
||||
5. Implement Steam Leaderboards:
|
||||
- Per-game leaderboards
|
||||
- Global cross-game leaderboard
|
||||
6. Add Steam Cloud saves:
|
||||
- Sync game progress across devices
|
||||
- Handle conflict resolution
|
||||
7. Integrate Steam Friends API:
|
||||
- "Invite friend to game room" button
|
||||
- Show online friends in lobby
|
||||
8. Test Steam Overlay functionality
|
||||
|
||||
**Deliverables:**
|
||||
- Full Steamworks integration
|
||||
- Achievement system (20-30 achievements)
|
||||
- Steam Leaderboards
|
||||
- Cloud saves
|
||||
- Friend invites
|
||||
|
||||
**Effort:** 100-120 hours
|
||||
**Cost:** $100 (Steam Direct fee, one-time, recoupable)
|
||||
|
||||
#### Phase 3: Store Page & Marketing Assets (2-3 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Create Steam store page:
|
||||
- Title: "Soroban Academy: Mathematical Strategy Games"
|
||||
- Capsule images (multiple sizes)
|
||||
- Hero image / header
|
||||
- 5-10 screenshots per game
|
||||
- 60-90 second trailer (show all games)
|
||||
- Detailed description with educational benefits
|
||||
- System requirements
|
||||
- Price: $9.99-14.99
|
||||
2. Write compelling marketing copy
|
||||
3. Create game trailer (video editing)
|
||||
4. Design Steam library assets
|
||||
5. Set up community hub
|
||||
6. Configure Steam tags:
|
||||
- Education, Strategy, Multiplayer, Casual, Math, Board Game
|
||||
7. Prepare press kit and media
|
||||
|
||||
**Deliverables:**
|
||||
- Complete Steam store page
|
||||
- Marketing trailer
|
||||
- Press kit
|
||||
- Community hub setup
|
||||
|
||||
**Effort:** 60-80 hours
|
||||
**Cost:** $0-300 (if hiring video editor)
|
||||
|
||||
#### Phase 4: Steam Review & Launch (2-3 weeks)
|
||||
|
||||
**Tasks:**
|
||||
1. Upload build to Steamworks
|
||||
2. Set release date (2+ weeks out)
|
||||
3. Make store page public
|
||||
4. Submit for Steam review (1-5 days)
|
||||
5. Address any review feedback
|
||||
6. Prepare launch marketing:
|
||||
- Email existing users
|
||||
- Social media campaign
|
||||
- Press outreach
|
||||
- Reddit/forum posts
|
||||
- Product Hunt launch
|
||||
7. Launch on Steam
|
||||
8. Monitor reviews and feedback
|
||||
9. Respond to community
|
||||
10. Plan post-launch updates
|
||||
|
||||
**Deliverables:**
|
||||
- Live on Steam
|
||||
- Launch marketing campaign
|
||||
- Community management process
|
||||
|
||||
**Effort:** 60-80 hours
|
||||
**Cost:** $0-500 (marketing budget)
|
||||
|
||||
#### Phase 5: Post-Launch Support (Ongoing)
|
||||
|
||||
**Tasks:**
|
||||
- Weekly bug fixes and patches
|
||||
- Monthly content updates
|
||||
- Seasonal events
|
||||
- New achievements
|
||||
- Community engagement
|
||||
- Steam Sale participation
|
||||
- User feedback integration
|
||||
|
||||
**Deliverables:**
|
||||
- Regular updates
|
||||
- Active community
|
||||
- Growing review score
|
||||
|
||||
**Effort:** 20-40 hours/month ongoing
|
||||
**Cost:** $0
|
||||
|
||||
### Total Timeline: 11-15 weeks
|
||||
### Total Effort: 340-430 hours
|
||||
### Total Cost: $100-900
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Functioning multiplayer system (✓ exists)
|
||||
- All games stable and bug-free
|
||||
- Marketing materials (screenshots, videos)
|
||||
- Legal documentation (EULA, privacy policy)
|
||||
- Support email/forum setup
|
||||
|
||||
### Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| Electron bundle too large | Medium | Medium | Optimize assets, use lazy loading |
|
||||
| Steamworks integration bugs | High | Medium | Extensive testing, use Greenworks |
|
||||
| Low sales | High | High | Strong marketing, community building |
|
||||
| Review bombing | Medium | Low | Active community management |
|
||||
| Multiplayer server costs | Medium | Medium | P2P option or Steam networking |
|
||||
| Steam review rejection | High | Low | Follow guidelines strictly |
|
||||
| Competition from free web version | High | High | Add Steam-exclusive features |
|
||||
|
||||
### Pricing Strategy
|
||||
|
||||
**Options:**
|
||||
1. **Premium ($9.99-14.99):** Full game bundle with Steam features
|
||||
2. **Freemium:** Free base + DLC for premium games
|
||||
3. **Early Access ($7.99):** Launch at lower price, increase at 1.0
|
||||
|
||||
**Recommendation:** Premium $12.99 with frequent sales
|
||||
- Perceived value: 6 games = ~$2 each
|
||||
- Recoup $100 fee after 8 sales
|
||||
- Sales can drive volume (50% off = $6.49)
|
||||
|
||||
### Marketing Angles for Steam
|
||||
|
||||
1. **Rithmomachia Focus:** "Play the 1,000-year-old medieval chess"
|
||||
2. **Educational Value:** "Math learning disguised as strategy gaming"
|
||||
3. **Multiplayer Fun:** "Challenge friends in mathematical duels"
|
||||
4. **Historical Gaming:** "Rediscover forgotten board games"
|
||||
5. **Family Friendly:** "Safe, educational gaming for all ages"
|
||||
|
||||
### Success Metrics
|
||||
|
||||
**Launch Goals:**
|
||||
- **Wishlist:** 1,000+ before launch
|
||||
- **Sales:** 500+ units in first month
|
||||
- **Reviews:** 50+ reviews, 85%+ positive
|
||||
- **Revenue:** $5,000+ in first quarter
|
||||
|
||||
**Long-term Goals:**
|
||||
- **Sales:** 5,000+ units in first year
|
||||
- **Reviews:** 200+ reviews, 90%+ positive
|
||||
- **Revenue:** $30,000+ annually
|
||||
- **Community:** Active Discord/forums with 1,000+ members
|
||||
|
||||
---
|
||||
|
||||
## Prioritization & Recommended Timeline
|
||||
|
||||
### Recommended Order of Execution
|
||||
|
||||
**Phase A: Quick Wins (First 3 months)**
|
||||
1. **Google Classroom - Phase 1 (OAuth/SSO)** - Immediate teacher value
|
||||
2. **CrazyGames submission** - Fast player discovery
|
||||
3. **Google Classroom - Phase 2 (Class Import)** - Core teacher feature
|
||||
|
||||
**Phase B: Institution Access (Months 4-6)**
|
||||
4. **Clever SSO integration** - Scale to districts
|
||||
5. **ClassLink SSO integration** - Additional district reach
|
||||
6. **Google Classroom - Phase 3 (Assignments)** - Deep integration
|
||||
|
||||
**Phase C: Casual Discovery (Months 6-9)**
|
||||
7. **Poki submission** - Largest casual audience
|
||||
8. **Kongregate pre-approval + submission** - Niche community
|
||||
|
||||
**Phase D: Premium Market (Months 9-12)**
|
||||
9. **Steam development** - Desktop packaging + Steamworks
|
||||
10. **Steam launch** - Premium educational game market
|
||||
|
||||
### Timeline Gantt Overview
|
||||
|
||||
```
|
||||
Month 1-2: [Google OAuth/SSO] [CrazyGames Prep]
|
||||
Month 2-3: [Google Class Import] [CrazyGames Launch]
|
||||
Month 4-5: [Clever SSO] [ClassLink SSO]
|
||||
Month 6-7: [Google Assignments] [Poki Prep]
|
||||
Month 7-8: [Poki Launch] [Kongregate]
|
||||
Month 9-10: [Steam Electron Build]
|
||||
Month 10-11: [Steam Steamworks Integration]
|
||||
Month 11-12: [Steam Store Page & Launch]
|
||||
```
|
||||
|
||||
### Total Cost Summary
|
||||
|
||||
| Platform | One-time Cost | Ongoing Cost | Notes |
|
||||
|----------|--------------|--------------|-------|
|
||||
| Google Classroom | $0 | $0 | Free for education |
|
||||
| Clever | $0 | $0 | Free for developers |
|
||||
| ClassLink | $0 | $0 | Free for developers |
|
||||
| CrazyGames | $0 | $0 | Revenue share via ads |
|
||||
| Poki | $0 | $0 | 50/50 revenue share |
|
||||
| Kongregate | $0 | $0 | Revenue share via ads |
|
||||
| Steam | $100 | $0-200/mo | Server costs optional |
|
||||
| **TOTAL** | **$100** | **$0-200/mo** | Very affordable |
|
||||
|
||||
### Resource Requirements
|
||||
|
||||
**Developer Time:**
|
||||
- **Total effort:** 1,140-1,450 hours across all platforms
|
||||
- **Timeline:** 9-12 months (with 1-2 developers)
|
||||
- **Ongoing:** 20-60 hours/month maintenance
|
||||
|
||||
**External Costs:**
|
||||
- Steam Direct fee: $100
|
||||
- Optional video editing: $0-500
|
||||
- Optional marketing budget: $0-1,000
|
||||
- Server costs (if needed): $50-200/month
|
||||
|
||||
**Skills Needed:**
|
||||
- OAuth/SAML implementation
|
||||
- REST API integration
|
||||
- Electron/desktop app packaging
|
||||
- Steamworks SDK integration
|
||||
- Marketing/community management
|
||||
|
||||
---
|
||||
|
||||
## Risk Management
|
||||
|
||||
### Top 5 Risks Across All Platforms
|
||||
|
||||
1. **Privacy Compliance (COPPA, FERPA, GDPR)**
|
||||
- **Impact:** Critical (could block educational adoption)
|
||||
- **Mitigation:** Legal review, implement consent systems, privacy policy updates
|
||||
|
||||
2. **Platform Rejection (Poki, Kongregate)**
|
||||
- **Impact:** High (wasted development time)
|
||||
- **Mitigation:** Apply early, start with less selective platforms, gather metrics
|
||||
|
||||
3. **Low Adoption/Sales**
|
||||
- **Impact:** High (ROI concern)
|
||||
- **Mitigation:** Strong marketing, community building, teacher outreach
|
||||
|
||||
4. **Technical Integration Complexity**
|
||||
- **Impact:** Medium (timeline delays)
|
||||
- **Mitigation:** Use established libraries, start simple, iterate
|
||||
|
||||
5. **Server Costs for Multiplayer**
|
||||
- **Impact:** Medium (ongoing expenses)
|
||||
- **Mitigation:** Optimize server efficiency, consider P2P, price accordingly
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics Dashboard
|
||||
|
||||
### Key Performance Indicators (KPIs)
|
||||
|
||||
**User Acquisition:**
|
||||
- New users per month (by source)
|
||||
- Sign-up conversion rate
|
||||
- Platform-specific downloads/plays
|
||||
|
||||
**Engagement:**
|
||||
- Daily/Monthly Active Users (DAU/MAU)
|
||||
- Average session length
|
||||
- Games per session
|
||||
- Return rate (D1, D7, D30)
|
||||
|
||||
**Education Impact:**
|
||||
- Teachers registered
|
||||
- Classes created
|
||||
- Assignments completed
|
||||
- Student progress metrics
|
||||
|
||||
**Revenue (Steam):**
|
||||
- Units sold
|
||||
- Revenue per month
|
||||
- Refund rate
|
||||
- Review score
|
||||
|
||||
**Platform-Specific:**
|
||||
- **Google Classroom:** Classes imported, assignments created
|
||||
- **Clever/ClassLink:** Districts connected, SSO logins
|
||||
- **Game Portals:** Plays per game, ad revenue, ratings
|
||||
- **Steam:** Sales, reviews, concurrent players
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate Actions (This Week)
|
||||
|
||||
1. **Decide on priority order** - Which platform to tackle first?
|
||||
2. **Set up project tracking** - Dedicate repository/board for integration work
|
||||
3. **Legal review** - Review privacy policy and terms for educational compliance
|
||||
4. **Create developer accounts:**
|
||||
- Google Cloud Platform
|
||||
- Clever Developer Portal
|
||||
- CrazyGames Developer Portal
|
||||
- Poki submission prep
|
||||
5. **Audit current codebase** - Check readiness for each integration
|
||||
|
||||
### Quick Wins to Start
|
||||
|
||||
**Option 1: Google Classroom OAuth (Fastest Impact)**
|
||||
- 2-3 weeks to launch
|
||||
- Immediate teacher value
|
||||
- No approval needed
|
||||
|
||||
**Option 2: CrazyGames (Fastest Player Growth)**
|
||||
- 3-4 weeks to launch
|
||||
- Immediate player discovery
|
||||
- Open platform (high acceptance rate)
|
||||
|
||||
**Recommendation:** Start both in parallel if resources allow
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### Useful Links
|
||||
|
||||
**Google Classroom:**
|
||||
- Developer Docs: https://developers.google.com/classroom
|
||||
- API Reference: https://developers.google.com/classroom/reference
|
||||
- OAuth Guide: https://developers.google.com/identity/protocols/oauth2
|
||||
|
||||
**Clever:**
|
||||
- Developer Portal: https://dev.clever.com
|
||||
- SSO Guide: https://dev.clever.com/docs/getting-started-with-clever-sso
|
||||
- API Docs: https://dev.clever.com/docs/api-overview
|
||||
|
||||
**ClassLink:**
|
||||
- SSO Library: https://www.classlink.com/resources/sso-search
|
||||
- OneRoster: https://www.imsglobal.org/activity/onerosterlis
|
||||
|
||||
**Game Portals:**
|
||||
- CrazyGames: https://docs.crazygames.com/
|
||||
- Poki: https://developers.poki.com/
|
||||
- Kongregate: BD@kongregate.com
|
||||
|
||||
**Steam:**
|
||||
- Steamworks: https://partner.steamgames.com/
|
||||
- Electron: https://www.electronjs.org/
|
||||
- Greenworks: https://github.com/greenheartgames/greenworks
|
||||
|
||||
### Code Library Recommendations
|
||||
|
||||
**OAuth/SSO:**
|
||||
- `next-auth` (already in use)
|
||||
- `passport-saml` (for ClassLink SAML)
|
||||
- `@googleapis/classroom` (official Google Classroom client)
|
||||
|
||||
**Electron:**
|
||||
- `electron-builder` (packaging)
|
||||
- `electron-updater` (auto-updates)
|
||||
- `greenworks` (Steamworks bridge)
|
||||
|
||||
**Game Portal SDKs:**
|
||||
- CrazyGames SDK (provided)
|
||||
- Poki SDK (provided)
|
||||
- Kongregate API (provided)
|
||||
|
||||
---
|
||||
|
||||
**Document End**
|
||||
|
||||
*For questions or updates to this roadmap, contact the development team.*
|
||||
@@ -1,390 +0,0 @@
|
||||
# PlayingGuideModal - Complete Feature Specification
|
||||
|
||||
## Overview
|
||||
Interactive, draggable, resizable modal for Rithmomachia game guide with i18n support and bust-out functionality.
|
||||
|
||||
## File Location
|
||||
`src/arcade-games/rithmomachia/components/PlayingGuideModal.tsx`
|
||||
|
||||
## Dependencies
|
||||
```typescript
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { Z_INDEX } from '@/constants/zIndex'
|
||||
import { useAbacusSettings } from '@/hooks/useAbacusSettings'
|
||||
import { PieceRenderer } from './PieceRenderer'
|
||||
import { RithmomachiaBoard, type ExamplePiece } from './RithmomachiaBoard'
|
||||
import type { PieceType, Color } from '../types'
|
||||
import '../i18n/config' // Initialize i18n
|
||||
```
|
||||
|
||||
## Props Interface
|
||||
```typescript
|
||||
interface PlayingGuideModalProps {
|
||||
isOpen: boolean // Controls visibility
|
||||
onClose: () => void // Called when modal closes
|
||||
standalone?: boolean // True when opened in popup window (full-screen mode)
|
||||
}
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Required State
|
||||
```typescript
|
||||
const { t, i18n } = useTranslation()
|
||||
const { data: abacusSettings } = useAbacusSettings()
|
||||
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
|
||||
|
||||
const [activeSection, setActiveSection] = useState<Section>('overview')
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
const [size, setSize] = useState({ width: 450, height: 600 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [resizeDirection, setResizeDirection] = useState<string>('')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
```
|
||||
|
||||
### Section Type
|
||||
```typescript
|
||||
type Section = 'overview' | 'pieces' | 'capture' | 'strategy' | 'harmony' | 'victory'
|
||||
```
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Radix Dialog Wrapper
|
||||
**When NOT standalone:**
|
||||
- Wrap entire modal in `<Dialog.Root open={isOpen} onOpenChange={onClose}>`
|
||||
- Use `<Dialog.Portal>` for portal rendering
|
||||
- Use `<Dialog.Overlay>` with backdrop styling
|
||||
- Use `<Dialog.Content>` as container for draggable/resizable content
|
||||
|
||||
**Styling:**
|
||||
- Overlay: semi-transparent black (`rgba(0, 0, 0, 0.5)`)
|
||||
- Content: no default positioning (we control via position state)
|
||||
- Z-index: Must be above game board - use `Z_INDEX.GAME.GUIDE_MODAL` or 15000+
|
||||
|
||||
**When standalone:**
|
||||
- Skip Dialog wrapper entirely
|
||||
- Render full-screen fixed container
|
||||
|
||||
### 2. Draggable Functionality
|
||||
|
||||
**Requirements:**
|
||||
- Click and drag from header to move modal
|
||||
- Disabled on mobile (`window.innerWidth < 768`)
|
||||
- Cursor changes to 'move' when hovering header
|
||||
- Position state tracks x, y coordinates
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (window.innerWidth < 768) return
|
||||
setIsDragging(true)
|
||||
setDragStart({
|
||||
x: e.clientX - position.x,
|
||||
y: e.clientY - position.y,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Global `mousemove` listener updates position while dragging
|
||||
- Global `mouseup` listener stops dragging
|
||||
- Cleanup listeners on unmount
|
||||
|
||||
### 3. Resizable Functionality
|
||||
|
||||
**Requirements:**
|
||||
- 8 resize handles: N, S, E, W, NE, NW, SE, SW
|
||||
- Handles visible only on hover (when `isHovered === true`)
|
||||
- Disabled on mobile
|
||||
- Min size: 450x600
|
||||
- Max size: 90vw x 80vh
|
||||
|
||||
**Handle Positions & Cursors:**
|
||||
- N (top): `cursor: 'ns-resize'`
|
||||
- S (bottom): `cursor: 'ns-resize'`
|
||||
- E (right): `cursor: 'ew-resize'`
|
||||
- W (left): `cursor: 'ew-resize'`
|
||||
- NE (top-right): `cursor: 'nesw-resize'`
|
||||
- NW (top-left): `cursor: 'nwse-resize'`
|
||||
- SE (bottom-right): `cursor: 'nwse-resize'`
|
||||
- SW (bottom-left): `cursor: 'nesw-resize'`
|
||||
|
||||
**Handle Styling:**
|
||||
- Width/height: 8px (invisible hit area)
|
||||
- Visible border when hovered: 2px solid blue
|
||||
- Positioned absolutely at edges/corners
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
const handleResizeStart = (e: React.MouseEvent, direction: string) => {
|
||||
if (window.innerWidth < 768) return
|
||||
e.stopPropagation()
|
||||
setIsResizing(true)
|
||||
setResizeDirection(direction)
|
||||
setDragStart({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Bust-Out Button
|
||||
|
||||
**Location:** Header, right side (before close button)
|
||||
|
||||
**Icon:** ↗ or external link icon
|
||||
|
||||
**Functionality:**
|
||||
```typescript
|
||||
const handleBustOut = () => {
|
||||
const url = window.location.origin + '/arcade/rithmomachia/guide'
|
||||
const features = 'width=600,height=800,menubar=no,toolbar=no,location=no,status=no'
|
||||
window.open(url, 'RithmomachiaGuide', features)
|
||||
}
|
||||
```
|
||||
|
||||
**Visibility:** Only show if NOT already standalone
|
||||
|
||||
**Route:** Must have a route at `/arcade/rithmomachia/guide` that renders:
|
||||
```tsx
|
||||
<PlayingGuideModal isOpen={true} onClose={() => window.close()} standalone={true} />
|
||||
```
|
||||
|
||||
### 5. Internationalization
|
||||
|
||||
**Setup:**
|
||||
- i18n config file: `src/arcade-games/rithmomachia/i18n/config.ts`
|
||||
- Translation files in: `src/arcade-games/rithmomachia/i18n/locales/`
|
||||
- Languages: en.json, de.json (minimum)
|
||||
|
||||
**Usage:**
|
||||
- All text uses `t('guide.section.key')` format
|
||||
- Language switcher in header with buttons for each language
|
||||
|
||||
**Header Language Switcher:**
|
||||
```tsx
|
||||
<div className={css({ display: 'flex', gap: '8px' })}>
|
||||
{['en', 'de'].map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => i18n.changeLanguage(lang)}
|
||||
className={css({
|
||||
px: '8px',
|
||||
py: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: i18n.language === lang ? 'bold' : 'normal',
|
||||
bg: i18n.language === lang ? '#3b82f6' : '#e5e7eb',
|
||||
color: i18n.language === lang ? 'white' : '#374151',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
>
|
||||
{lang.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 6. Centering on Mount
|
||||
|
||||
**Effect:**
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (isOpen && modalRef.current && !standalone) {
|
||||
const rect = modalRef.current.getBoundingClientRect()
|
||||
setPosition({
|
||||
x: (window.innerWidth - rect.width) / 2,
|
||||
y: Math.max(50, (window.innerHeight - rect.height) / 2),
|
||||
})
|
||||
}
|
||||
}, [isOpen, standalone])
|
||||
```
|
||||
|
||||
**Standalone Mode:**
|
||||
- If standalone, don't center - use full viewport
|
||||
- Position: fixed, top: 0, left: 0, width: 100vw, height: 100vh
|
||||
|
||||
## Layout Structure
|
||||
|
||||
```
|
||||
<Dialog.Root> (if not standalone)
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content asChild>
|
||||
<div ref={modalRef} style={{ position: absolute, top: position.y, left: position.x }}>
|
||||
{/* Resize handles (8 total, only if hovered and not mobile) */}
|
||||
|
||||
<div> {/* Main container */}
|
||||
{/* Header */}
|
||||
<div onMouseDown={handleMouseDown} style={{ cursor: isDragging ? 'grabbing' : 'grab' }}>
|
||||
<h2>{t('guide.title')}</h2>
|
||||
<div> {/* Language switcher */}
|
||||
<button onClick={handleBustOut}> {/* Bust-out (if not standalone) */}
|
||||
<button onClick={onClose}> {/* Close X */}
|
||||
</div>
|
||||
|
||||
{/* Navigation tabs */}
|
||||
<div> {/* Section buttons: Overview, Pieces, Capture, Strategy, Harmony, Victory */}
|
||||
|
||||
{/* Content area - scrollable */}
|
||||
<div style={{ overflow: 'auto', maxHeight: size.height - headerHeight }}>
|
||||
{activeSection === 'overview' && <OverviewSection />}
|
||||
{activeSection === 'pieces' && <PiecesSection useNativeAbacusNumbers={useNativeAbacusNumbers} />}
|
||||
{activeSection === 'capture' && <CaptureSection />}
|
||||
{/* ... etc */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
```
|
||||
|
||||
## Styling Requirements
|
||||
|
||||
### Main Container
|
||||
- Background: `#ffffff`
|
||||
- Border radius: `12px`
|
||||
- Box shadow: `0 20px 60px rgba(0, 0, 0, 0.3)`
|
||||
- Border: `1px solid #e5e7eb`
|
||||
- Position: `absolute` (controlled by position state)
|
||||
- Width/height: from size state
|
||||
|
||||
### Header
|
||||
- Background: `#f9fafb`
|
||||
- Border bottom: `1px solid #e5e7eb`
|
||||
- Padding: `16px`
|
||||
- Display: flex, justify-between, align-items: center
|
||||
- Cursor: `move` on desktop (when not standalone)
|
||||
- Prevent text selection while dragging
|
||||
|
||||
### Navigation Tabs
|
||||
- Display: flex, gap: `8px`
|
||||
- Padding: `12px 16px`
|
||||
- Background: `#ffffff`
|
||||
- Border bottom: `1px solid #e5e7eb`
|
||||
|
||||
### Tab Buttons
|
||||
- Active: bold, blue background, white text
|
||||
- Inactive: normal weight, gray background, dark text
|
||||
- Padding: `8px 16px`
|
||||
- Border radius: `6px`
|
||||
- Cursor: pointer
|
||||
- Transition: all 0.2s
|
||||
|
||||
### Content Area
|
||||
- Padding: `24px`
|
||||
- Overflow: auto
|
||||
- Max height: calculated (size.height - header - tabs)
|
||||
- Color: `#374151`
|
||||
- Line height: `1.6`
|
||||
|
||||
### Resize Handles
|
||||
- Position: absolute
|
||||
- Width/height: 8px
|
||||
- Background: transparent
|
||||
- Border: visible on hover (2px solid `#3b82f6`)
|
||||
- Z-index: 1 (above content)
|
||||
|
||||
## Content Sections
|
||||
|
||||
### PiecesSection Component
|
||||
**Must have its own useAbacusSettings hook:**
|
||||
```typescript
|
||||
function PiecesSection() {
|
||||
const { data: abacusSettings } = useAbacusSettings()
|
||||
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
|
||||
|
||||
// ... piece rendering with useNativeAbacusNumbers prop
|
||||
}
|
||||
```
|
||||
|
||||
### All RithmomachiaBoard Uses
|
||||
- Must pass `useNativeAbacusNumbers={useNativeAbacusNumbers}` prop
|
||||
- Boards show game positions with pieces
|
||||
|
||||
### All PieceRenderer Uses
|
||||
- Must pass `useNativeAbacusNumbers={useNativeAbacusNumbers}` prop
|
||||
- Renders individual piece icons in pieces section
|
||||
|
||||
## Translation Keys (Minimum Required)
|
||||
|
||||
```json
|
||||
{
|
||||
"guide": {
|
||||
"title": "Rithmomachia Playing Guide",
|
||||
"overview": {
|
||||
"title": "Overview",
|
||||
"content": "..."
|
||||
},
|
||||
"pieces": {
|
||||
"title": "Your Pieces",
|
||||
"circle": "Circle",
|
||||
"triangle": "Triangle",
|
||||
"square": "Square",
|
||||
"pyramid": "Pyramid"
|
||||
},
|
||||
"capture": {
|
||||
"title": "Capture Rules",
|
||||
"equality": "Equality",
|
||||
"multiple": "Multiple",
|
||||
"ratio": "Ratio",
|
||||
"sum": "Sum",
|
||||
"difference": "Difference",
|
||||
"product": "Product"
|
||||
},
|
||||
"strategy": {
|
||||
"title": "Strategy Tips"
|
||||
},
|
||||
"harmony": {
|
||||
"title": "Harmony (Progressions)"
|
||||
},
|
||||
"victory": {
|
||||
"title": "Victory Conditions"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Prevention
|
||||
|
||||
1. **Z-Index Issue:** Must be higher than game board (use `Z_INDEX.GAME.GUIDE_MODAL` or 15000+)
|
||||
2. **Lost Work:** Never use `git checkout --` on working files without confirming stash/commit first
|
||||
3. **Dialog Overlay:** Must render with high z-index to cover game
|
||||
4. **Mobile:** Disable drag/resize on mobile, make responsive
|
||||
5. **Standalone Route:** Must exist at `/arcade/rithmomachia/guide`
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Modal opens and closes correctly
|
||||
- [ ] Dragging works on desktop
|
||||
- [ ] Resizing works on desktop (all 8 handles)
|
||||
- [ ] Drag/resize disabled on mobile
|
||||
- [ ] Language switcher changes content
|
||||
- [ ] Bust-out button opens new window
|
||||
- [ ] New window renders standalone mode correctly
|
||||
- [ ] Modal appears above game board
|
||||
- [ ] Close button works
|
||||
- [ ] All sections render correctly
|
||||
- [ ] Native abacus numbers toggle respected
|
||||
- [ ] Translations load for all languages
|
||||
- [ ] Modal centers on first open
|
||||
- [ ] Position/size persists while open
|
||||
- [ ] Cleanup happens on unmount
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. Basic Dialog structure with standalone mode
|
||||
2. Header with title, close, bust-out
|
||||
3. Language switcher and i18n setup
|
||||
4. Navigation tabs
|
||||
5. Content sections (start with existing content)
|
||||
6. Dragging functionality
|
||||
7. Resizing functionality
|
||||
8. Native abacus numbers integration
|
||||
9. Translation files
|
||||
10. Standalone route page
|
||||
@@ -104,69 +104,9 @@
|
||||
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')",
|
||||
"Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')",
|
||||
"Bash(git rev-parse HEAD)",
|
||||
"Bash(gh run watch --exit-status 18662351595)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:www.npmjs.com)",
|
||||
"mcp__sqlite__list_tables",
|
||||
"mcp__sqlite__describe_table",
|
||||
"mcp__sqlite__read_query",
|
||||
"Bash(git rebase:*)",
|
||||
"Bash(gh run watch:*)",
|
||||
"Bash(git reflog:*)",
|
||||
"Bash(do echo -e \"\\n$hash:\")",
|
||||
"Bash(git fsck:*)",
|
||||
"Bash(do echo \"=== Stash @{$i} ===\")",
|
||||
"Bash(git diff-tree:*)",
|
||||
"Bash(git merge-base:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(while read file)",
|
||||
"Bash(do if git show HEAD:\"$file\")",
|
||||
"Bash(/dev/null)",
|
||||
"Bash(then echo \"✓ $file\")",
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(node scripts/parseBoardCSV.js:*)",
|
||||
"Bash(do echo \"=== HEAD~$i ===\")",
|
||||
"Read(//private/tmp/**)",
|
||||
"Bash(do echo \"=== $commit ===\")",
|
||||
"Bash(do echo \"=== stash@{$i} ===\")",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(jq:*)",
|
||||
"Bash(src/arcade-games/rithmomachia/components/guide-sections/OverviewSection.tsx )",
|
||||
"Bash(src/arcade-games/rithmomachia/components/guide-sections/PiecesSection.tsx )",
|
||||
"Bash(src/arcade-games/rithmomachia/components/guide-sections/CaptureSection.tsx )",
|
||||
"Bash(src/arcade-games/rithmomachia/components/guide-sections/HarmonySection.tsx )",
|
||||
"Bash(src/arcade-games/rithmomachia/components/guide-sections/VictorySection.tsx)",
|
||||
"Bash(pnpm remove:*)",
|
||||
"Bash(__NEW_LINE__ sed -n '68,73p' CaptureSection.tsx.bak)",
|
||||
"WebFetch(domain:hub.docker.com)",
|
||||
"Bash(gcloud auth:*)",
|
||||
"Bash(gcloud config list:*)",
|
||||
"WebFetch(domain:www.boardspace.net)",
|
||||
"WebFetch(domain:www.gamecabinet.com)",
|
||||
"WebFetch(domain:en.wikipedia.org)",
|
||||
"Bash(pnpm search:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(timeout 10 npx drizzle-kit generate:sqlite:*)",
|
||||
"Bash(brew install:*)",
|
||||
"Bash(sudo ln:*)",
|
||||
"Bash(cd:*)",
|
||||
"Bash(git clone:*)",
|
||||
"Bash(git ls-remote:*)",
|
||||
"Bash(openscad:*)",
|
||||
"Bash(npx eslint:*)",
|
||||
"Bash(env)",
|
||||
"Bash(security find-generic-password -s 'Anthropic API Key' -w)",
|
||||
"Bash(printenv:*)",
|
||||
"Bash(typst:*)",
|
||||
"Bash(npx tsx:*)",
|
||||
"Bash(sort:*)",
|
||||
"Bash(scp:*)"
|
||||
"Bash(gh run watch --exit-status 18662351595)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": ["sqlite"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Test deployment - Mon Nov 3 16:31:57 CST 2025
|
||||
@@ -1,437 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { db, schema } from '../src/db'
|
||||
import { createRoom } from '../src/lib/arcade/room-manager'
|
||||
import { createInvitation, getInvitation } from '../src/lib/arcade/room-invitations'
|
||||
import { addRoomMember } from '../src/lib/arcade/room-membership'
|
||||
|
||||
/**
|
||||
* Join Flow with Invitation Acceptance E2E Tests
|
||||
*
|
||||
* Tests the bug fix for invitation acceptance:
|
||||
* - When a user joins a restricted room with an invitation
|
||||
* - The invitation should be marked as "accepted"
|
||||
* - This prevents the invitation from showing up again
|
||||
*
|
||||
* Regression test for the bug where invitations stayed "pending" forever.
|
||||
*/
|
||||
|
||||
describe('Join Flow: Invitation Acceptance', () => {
|
||||
let hostUserId: string
|
||||
let guestUserId: string
|
||||
let hostGuestId: string
|
||||
let guestGuestId: string
|
||||
let roomId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test users
|
||||
hostGuestId = `test-host-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
guestGuestId = `test-guest-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
|
||||
const [host] = await db.insert(schema.users).values({ guestId: hostGuestId }).returning()
|
||||
const [guest] = await db.insert(schema.users).values({ guestId: guestGuestId }).returning()
|
||||
|
||||
hostUserId = host.id
|
||||
guestUserId = guest.id
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up invitations
|
||||
if (roomId) {
|
||||
await db.delete(schema.roomInvitations).where(eq(schema.roomInvitations.roomId, roomId))
|
||||
}
|
||||
|
||||
// Clean up room
|
||||
if (roomId) {
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, roomId))
|
||||
}
|
||||
|
||||
// Clean up users
|
||||
await db.delete(schema.users).where(eq(schema.users.id, hostUserId))
|
||||
await db.delete(schema.users).where(eq(schema.users.id, guestUserId))
|
||||
})
|
||||
|
||||
describe('BUG FIX: Invitation marked as accepted after join', () => {
|
||||
it('marks invitation as accepted when guest joins restricted room', async () => {
|
||||
// 1. Host creates a restricted room
|
||||
const room = await createRoom({
|
||||
name: 'Restricted Room',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host User',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted', // Requires invitation
|
||||
})
|
||||
roomId = room.id
|
||||
|
||||
// 2. Host invites guest
|
||||
const invitation = await createInvitation({
|
||||
roomId,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest User',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host User',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
// 3. Verify invitation is pending
|
||||
expect(invitation.status).toBe('pending')
|
||||
|
||||
// 4. Guest joins the room (simulating the join API flow)
|
||||
// In the real API, it checks the invitation and then adds the member
|
||||
const invitationCheck = await getInvitation(roomId, guestUserId)
|
||||
expect(invitationCheck?.status).toBe('pending')
|
||||
|
||||
// Simulate what the join API does: add member
|
||||
await addRoomMember({
|
||||
roomId,
|
||||
userId: guestGuestId,
|
||||
displayName: 'Guest User',
|
||||
isCreator: false,
|
||||
})
|
||||
|
||||
// 5. BUG: Before fix, invitation would still be "pending" here
|
||||
// AFTER FIX: The join API now explicitly marks it as "accepted"
|
||||
|
||||
// Simulate the fix from join API
|
||||
const { acceptInvitation } = await import('../src/lib/arcade/room-invitations')
|
||||
await acceptInvitation(invitation.id)
|
||||
|
||||
// 6. Verify invitation is now marked as accepted
|
||||
const updatedInvitation = await getInvitation(roomId, guestUserId)
|
||||
expect(updatedInvitation?.status).toBe('accepted')
|
||||
expect(updatedInvitation?.respondedAt).toBeDefined()
|
||||
})
|
||||
|
||||
it('prevents showing the same invitation again after accepting', async () => {
|
||||
// This tests the exact bug scenario from the issue:
|
||||
// "even if I accept the invite and join the room,
|
||||
// if I try to join room SFK3GD again, then I'm shown the same invite notice"
|
||||
|
||||
// 1. Create Room A and Room B
|
||||
const roomA = await createRoom({
|
||||
name: 'Room KHS3AE',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
|
||||
const roomB = await createRoom({
|
||||
name: 'Room SFK3GD',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'open', // Guest can join without invitation
|
||||
})
|
||||
|
||||
roomId = roomA.id // For cleanup
|
||||
|
||||
// 2. Invite guest to Room A
|
||||
const invitationA = await createInvitation({
|
||||
roomId: roomA.id,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
// 3. Guest sees invitation to Room A
|
||||
const { getUserPendingInvitations } = await import('../src/lib/arcade/room-invitations')
|
||||
let pendingInvites = await getUserPendingInvitations(guestUserId)
|
||||
expect(pendingInvites).toHaveLength(1)
|
||||
expect(pendingInvites[0].roomId).toBe(roomA.id)
|
||||
|
||||
// 4. Guest accepts and joins Room A
|
||||
const { acceptInvitation } = await import('../src/lib/arcade/room-invitations')
|
||||
await acceptInvitation(invitationA.id)
|
||||
|
||||
await addRoomMember({
|
||||
roomId: roomA.id,
|
||||
userId: guestGuestId,
|
||||
displayName: 'Guest',
|
||||
isCreator: false,
|
||||
})
|
||||
|
||||
// 5. Guest tries to visit Room B link (/join/SFK3GD)
|
||||
// BUG: Before fix, they'd see Room A invitation again because it's still "pending"
|
||||
// FIX: Invitation is now "accepted", so it won't show in pending list
|
||||
|
||||
pendingInvites = await getUserPendingInvitations(guestUserId)
|
||||
expect(pendingInvites).toHaveLength(0) // ✅ No longer shows Room A
|
||||
|
||||
// 6. Guest can successfully join Room B without being interrupted
|
||||
await addRoomMember({
|
||||
roomId: roomB.id,
|
||||
userId: guestGuestId,
|
||||
displayName: 'Guest',
|
||||
isCreator: false,
|
||||
})
|
||||
|
||||
// Clean up
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, roomB.id))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Invitation flow with multiple rooms', () => {
|
||||
it('only shows pending invitations, not accepted ones', async () => {
|
||||
// Create 3 rooms, invite to all of them
|
||||
const room1 = await createRoom({
|
||||
name: 'Room 1',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
|
||||
const room2 = await createRoom({
|
||||
name: 'Room 2',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
|
||||
const room3 = await createRoom({
|
||||
name: 'Room 3',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
|
||||
roomId = room1.id // For cleanup
|
||||
|
||||
// Invite to all 3
|
||||
const inv1 = await createInvitation({
|
||||
roomId: room1.id,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
const inv2 = await createInvitation({
|
||||
roomId: room2.id,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
const inv3 = await createInvitation({
|
||||
roomId: room3.id,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
// All 3 should be pending
|
||||
const { getUserPendingInvitations, acceptInvitation } = await import(
|
||||
'../src/lib/arcade/room-invitations'
|
||||
)
|
||||
let pending = await getUserPendingInvitations(guestUserId)
|
||||
expect(pending).toHaveLength(3)
|
||||
|
||||
// Accept invitation 1 and join
|
||||
await acceptInvitation(inv1.id)
|
||||
|
||||
// Now only 2 should be pending
|
||||
pending = await getUserPendingInvitations(guestUserId)
|
||||
expect(pending).toHaveLength(2)
|
||||
expect(pending.map((p) => p.roomId)).not.toContain(room1.id)
|
||||
|
||||
// Clean up
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, room2.id))
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, room3.id))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Host re-joining their own restricted room', () => {
|
||||
it('host can rejoin without invitation (no acceptance needed)', async () => {
|
||||
// Create restricted room as host
|
||||
const room = await createRoom({
|
||||
name: 'Host Room',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host User',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
roomId = room.id
|
||||
|
||||
// Host joins their own room
|
||||
await addRoomMember({
|
||||
roomId,
|
||||
userId: hostGuestId,
|
||||
displayName: 'Host User',
|
||||
isCreator: true,
|
||||
})
|
||||
|
||||
// No invitation needed, no acceptance
|
||||
// This should not create any invitation records
|
||||
const invitation = await getInvitation(roomId, hostUserId)
|
||||
expect(invitation).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('handles multiple invitations from same host to same guest (updates, not duplicates)', async () => {
|
||||
const room = await createRoom({
|
||||
name: 'Test Room',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
roomId = room.id
|
||||
|
||||
// Send first invitation
|
||||
const inv1 = await createInvitation({
|
||||
roomId,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
message: 'First message',
|
||||
})
|
||||
|
||||
// Send second invitation (should update, not create new)
|
||||
const inv2 = await createInvitation({
|
||||
roomId,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
message: 'Second message',
|
||||
})
|
||||
|
||||
// Should be same invitation (same ID)
|
||||
expect(inv1.id).toBe(inv2.id)
|
||||
expect(inv2.message).toBe('Second message')
|
||||
|
||||
// Should only have 1 invitation in database
|
||||
const allInvitations = await db
|
||||
.select()
|
||||
.from(schema.roomInvitations)
|
||||
.where(eq(schema.roomInvitations.roomId, roomId))
|
||||
|
||||
expect(allInvitations).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('re-sends invitation after previous was declined', async () => {
|
||||
const room = await createRoom({
|
||||
name: 'Test Room',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'restricted',
|
||||
})
|
||||
roomId = room.id
|
||||
|
||||
// First invitation
|
||||
const inv1 = await createInvitation({
|
||||
roomId,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
// Guest declines
|
||||
const { declineInvitation, getUserPendingInvitations } = await import(
|
||||
'../src/lib/arcade/room-invitations'
|
||||
)
|
||||
await declineInvitation(inv1.id)
|
||||
|
||||
// Should not be in pending list
|
||||
let pending = await getUserPendingInvitations(guestUserId)
|
||||
expect(pending).toHaveLength(0)
|
||||
|
||||
// Host sends new invitation (should reset to pending)
|
||||
await createInvitation({
|
||||
roomId,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
// Should now be in pending list again
|
||||
pending = await getUserPendingInvitations(guestUserId)
|
||||
expect(pending).toHaveLength(1)
|
||||
expect(pending[0].status).toBe('pending')
|
||||
})
|
||||
|
||||
it('accepts invitations to OPEN rooms (not just restricted)', async () => {
|
||||
// This tests the root cause of the bug:
|
||||
// Invitations to OPEN rooms were never being marked as accepted
|
||||
|
||||
const openRoom = await createRoom({
|
||||
name: 'Open Room',
|
||||
createdBy: hostGuestId,
|
||||
creatorName: 'Host',
|
||||
gameName: 'rithmomachia',
|
||||
gameConfig: {},
|
||||
accessMode: 'open', // Open access - no invitation required to join
|
||||
})
|
||||
roomId = openRoom.id
|
||||
|
||||
// Host sends invitation anyway (e.g., to notify guest about the room)
|
||||
const inv = await createInvitation({
|
||||
roomId: openRoom.id,
|
||||
userId: guestUserId,
|
||||
userName: 'Guest',
|
||||
invitedBy: hostUserId,
|
||||
invitedByName: 'Host',
|
||||
invitationType: 'manual',
|
||||
})
|
||||
|
||||
// Guest should see pending invitation
|
||||
const { getUserPendingInvitations, acceptInvitation } = await import(
|
||||
'../src/lib/arcade/room-invitations'
|
||||
)
|
||||
let pending = await getUserPendingInvitations(guestUserId)
|
||||
expect(pending).toHaveLength(1)
|
||||
|
||||
// Guest joins the open room (invitation not required, but present)
|
||||
await addRoomMember({
|
||||
roomId: openRoom.id,
|
||||
userId: guestGuestId,
|
||||
displayName: 'Guest',
|
||||
isCreator: false,
|
||||
})
|
||||
|
||||
// Simulate the join API accepting the invitation
|
||||
await acceptInvitation(inv.id)
|
||||
|
||||
// BUG FIX: Invitation should now be accepted, not stuck in pending
|
||||
pending = await getUserPendingInvitations(guestUserId)
|
||||
expect(pending).toHaveLength(0) // ✅ No longer pending
|
||||
|
||||
// Verify it's marked as accepted
|
||||
const acceptedInv = await getInvitation(openRoom.id, guestUserId)
|
||||
expect(acceptedInv?.status).toBe('accepted')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,195 +0,0 @@
|
||||
---
|
||||
title: "Beyond Easy and Hard: A 2D Approach to Worksheet Difficulty"
|
||||
description: "Most educational software uses a simple 1D difficulty slider. We built something better: a constrained 2D space that separates problem complexity from instructional support."
|
||||
author: "Abaci.one Team"
|
||||
publishedAt: "2025-11-07"
|
||||
updatedAt: "2025-11-07"
|
||||
tags: ["education", "difficulty", "pedagogy", "soroban", "worksheets"]
|
||||
featured: true
|
||||
---
|
||||
|
||||
# Beyond Easy and Hard: A 2D Approach to Worksheet Difficulty
|
||||
|
||||
Most educational software treats difficulty as a one-dimensional slider: easy → medium → hard. But anyone who's taught students knows that difficulty is more nuanced than that.
|
||||
|
||||
We've built a new approach for our addition worksheet generator that treats difficulty as **two independent dimensions**: problem complexity (Challenge) and instructional support (Support). And critically, we constrain the combinations to only those that are pedagogically valid.
|
||||
|
||||
Here's why this matters and how it works.
|
||||
|
||||
## The Problem with 1D Difficulty
|
||||
|
||||
Imagine you're a teacher working with two students:
|
||||
|
||||
**Student A**: Ready for harder problems with multi-digit regrouping, but still benefits from visual aids like ten-frames and place value colors.
|
||||
|
||||
**Student B**: Comfortable working independently without scaffolding, but struggles with complex regrouping and needs simpler problems.
|
||||
|
||||
With a traditional "easy/medium/hard" system, you're stuck:
|
||||
- Setting difficulty to "hard" gives Student A complex problems... but removes all the visual support they still need
|
||||
- Setting it to "easy" gives Student B the scaffolding-free experience they want... but the problems are too simple
|
||||
|
||||
**You can't express "hard problems with visual aids" or "easy problems without scaffolding"** because difficulty conflates two completely different things: the intrinsic complexity of the problem and the amount of instructional support provided.
|
||||
|
||||
## Our Solution: Challenge × Support
|
||||
|
||||
We split difficulty into two independent dimensions:
|
||||
|
||||
### Challenge Axis (Regrouping Complexity)
|
||||
How complex is the problem itself?
|
||||
- **Low**: Simple addition, no carrying (23 + 15)
|
||||
- **Medium**: Some regrouping in ones or tens place (47 + 38)
|
||||
- **High**: Frequent regrouping across multiple place values (587 + 798)
|
||||
|
||||
This is **intrinsic cognitive load** — the inherent difficulty of the problem regardless of how it's presented.
|
||||
|
||||
### Support Axis (Scaffolding Level)
|
||||
How much instructional support is shown?
|
||||
- **High support**: Carry boxes, answer boxes, place value colors, ten-frames
|
||||
- **Medium support**: Carry boxes when needed, colors for larger numbers
|
||||
- **Low support**: Minimal or no scaffolding, student works independently
|
||||
|
||||
This is **extraneous cognitive load** — the mental effort required by how the problem is presented and supported.
|
||||
|
||||
## But Here's the Crucial Part: Constraints
|
||||
|
||||
Not all combinations of Challenge and Support are pedagogically valid.
|
||||
|
||||
**High challenge + High support** doesn't work well. If you're giving students complex multi-digit regrouping problems but showing them every step with maximum scaffolding, you're preventing them from developing problem-solving strategies. They're just following the scaffolds, not thinking.
|
||||
|
||||
**Low challenge + Low support** is pointless practice. If the problems are trivially simple and you're not providing any instructional structure, students aren't learning anything new.
|
||||
|
||||
So we constrain the space to a **diagonal band** of valid combinations:
|
||||
|
||||
```
|
||||
Support (Scaffolding) →
|
||||
Low Medium High
|
||||
Challenge High ✓ ✓ ✗
|
||||
(Regrouping) ✓ ✓ ✓
|
||||
Medium ✗ ✓ ✓
|
||||
✗ ✗ ✓
|
||||
Low ✗ ✓ ✓
|
||||
```
|
||||
|
||||
**As challenge increases, support must decrease** (and vice versa). This encodes a fundamental pedagogical principle: students learning new concepts need support, but as they master the concept, support should fade.
|
||||
|
||||
### Visual Examples
|
||||
|
||||
Here's what this looks like in practice. Below are actual worksheet examples showing **the same problem complexity** (problems with moderate regrouping) but with **different levels of scaffolding**:
|
||||
|
||||
#### Full Scaffolding
|
||||

|
||||
*Maximum visual support: carry boxes always shown, answer boxes, place value colors, and ten-frames for every step.*
|
||||
|
||||
#### Medium Scaffolding
|
||||

|
||||
*Strategic support: carry boxes appear when regrouping occurs, answer boxes provided, place value colors for 3+ digit numbers.*
|
||||
|
||||
#### Minimal Scaffolding
|
||||

|
||||
*Minimal scaffolding: carry boxes only for complex problems with multiple regroups, no answer boxes or colors.*
|
||||
|
||||
#### No Scaffolding
|
||||

|
||||
*Zero scaffolding: students work completely independently with no visual aids.*
|
||||
|
||||
Notice how the **problem complexity stays constant** (all use the same regrouping probability), but the **scaffolding progressively fades**. This demonstrates how support can be adjusted independently from problem difficulty, allowing teachers to precisely target their students' needs.
|
||||
|
||||
## Theoretical Foundation
|
||||
|
||||
This isn't just intuition — it maps to established learning theory:
|
||||
|
||||
**Zone of Proximal Development** (Vygotsky): The diagonal band represents the learnable space. Too easy = already mastered. Too hard without support = beyond reach. The valid combinations are where learning happens.
|
||||
|
||||
**Cognitive Load Theory** (Sweller): Effective instruction balances intrinsic load (problem complexity) and extraneous load (instructional design). Our constraints prevent overload from either source.
|
||||
|
||||
**Scaffolding Fading** (Wood, Bruner, Ross): Temporary supports should be gradually removed as competence develops. The constraint band enforces this fading principle.
|
||||
|
||||
## How Teachers Use It
|
||||
|
||||
The UI provides three ways to adjust difficulty:
|
||||
|
||||
### 1. Default: "Make Harder" / "Make Easier"
|
||||
The main buttons adjust **both dimensions** simultaneously, moving diagonally through the valid space toward appropriate preset levels (Beginner → Early Learner → Intermediate → Advanced → Expert).
|
||||
|
||||
This is the simple, no-thought-required option that works for most cases.
|
||||
|
||||
### 2. Challenge-Only Adjustment
|
||||
Click the dropdown arrow, select "More challenge" or "Less challenge".
|
||||
|
||||
This moves **horizontally** — changing problem complexity while maintaining current scaffolding level.
|
||||
|
||||
**Use case**: Student A above. They're ready for harder problems but still need the visual aids. Click "More challenge" to increase regrouping while keeping support constant.
|
||||
|
||||
### 3. Support-Only Adjustment
|
||||
Click the dropdown arrow, select "More support" or "Less support".
|
||||
|
||||
This moves **vertically** — changing scaffolding level while maintaining current problem complexity.
|
||||
|
||||
**Use case**: Student B above. They understand the concepts and don't need the training wheels anymore. Click "Less support" to remove scaffolding while keeping problems at the same complexity.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
Under the hood, we use a **hybrid discrete/continuous architecture**:
|
||||
|
||||
- **Discrete indices** for navigation: 19 regrouping levels (0-18), 13 scaffolding levels (0-12)
|
||||
- **Continuous scores** for visualization: Calculated on-the-fly for the difficulty graph and preset detection
|
||||
- **Constraint validation** at every step: The system auto-corrects invalid states
|
||||
|
||||
This gives us:
|
||||
- Predictable, testable behavior (discrete states)
|
||||
- Smooth visualization (continuous scores)
|
||||
- Guaranteed pedagogical validity (constraint enforcement)
|
||||
|
||||
Each preset profile (Beginner/Intermediate/etc.) is a specific (challenge, support) coordinate in the valid space. The "Make Harder" button finds the nearest harder preset and navigates toward it, automatically adjusting both dimensions as needed.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
The system is live at **[abaci.one/create/worksheets/addition](https://abaci.one/create/worksheets/addition)**.
|
||||
|
||||
Try these scenarios:
|
||||
|
||||
1. **Start at Beginner**, click "Make Harder" repeatedly → watch it move diagonally through the space
|
||||
2. **Start at Intermediate**, use the dropdown to select "More challenge" only → see problems get harder while keeping visual aids
|
||||
3. **Start at Early Learner**, use "Less support" → watch scaffolding disappear while problem complexity stays constant
|
||||
4. **Click on the 2D graph** (the orange debug visualization) → jump directly to any valid difficulty point
|
||||
|
||||
The graph shows:
|
||||
- Gray diagonal band: Valid pedagogical combinations
|
||||
- Colored dots: Preset profiles (B=Beginner, I=Intermediate, etc.)
|
||||
- Blue cross: Your current position
|
||||
- Click anywhere to jump there (system auto-corrects to nearest valid point)
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Traditional 1D difficulty forces teachers into a one-size-fits-all progression. Every student moves along the same path from "easy" to "hard", regardless of their individual needs.
|
||||
|
||||
**Our 2D constrained space enables precise differentiation**:
|
||||
- Students who grasp concepts quickly can reduce support while maintaining challenge
|
||||
- Students who need more time get continued support while still progressing to harder problems
|
||||
- Students can move through the space at different angles, not just along a single path
|
||||
|
||||
And because the constraints encode pedagogical principles, teachers can't accidentally create nonsensical combinations. The system guides them toward valid instructional choices.
|
||||
|
||||
## What's Next
|
||||
|
||||
This is currently implemented for addition worksheets, but the approach generalizes:
|
||||
- Subtraction, multiplication, division
|
||||
- Other domains entirely (reading comprehension, programming exercises, etc.)
|
||||
- Any learning task where you can separate intrinsic difficulty from instructional support
|
||||
|
||||
The code is **open source**: [github.com/antialias/soroban-abacus-flashcards](https://github.com/antialias/soroban-abacus-flashcards)
|
||||
|
||||
Technical details: [SMART_DIFFICULTY_SPEC.md](https://github.com/antialias/soroban-abacus-flashcards/blob/main/apps/web/src/app/create/worksheets/addition/SMART_DIFFICULTY_SPEC.md)
|
||||
|
||||
## Feedback Welcome
|
||||
|
||||
We'd love to hear from educators using this system:
|
||||
- Does the 2D model match your mental model of difficulty?
|
||||
- Are the dimension-specific controls useful?
|
||||
- What other domains would benefit from this approach?
|
||||
|
||||
Reach out via [GitHub issues](https://github.com/antialias/soroban-abacus-flashcards/issues) or try the system and let us know what you think.
|
||||
|
||||
---
|
||||
|
||||
*This post describes research-in-progress. We're exploring publication in learning sciences venues (ACM Learning @ Scale, IJAIED). If you're interested in collaboration or want to cite this work, see our [publication plan](https://github.com/antialias/soroban-abacus-flashcards/blob/main/apps/web/src/app/create/worksheets/addition/PUBLICATION_PLAN.md).*
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
-- Add native_abacus_numbers column to abacus_settings table
|
||||
ALTER TABLE `abacus_settings` ADD `native_abacus_numbers` integer DEFAULT 0 NOT NULL;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- Migration: Add player_stats table
|
||||
-- Per-player game statistics tracking
|
||||
|
||||
CREATE TABLE `player_stats` (
|
||||
`player_id` text PRIMARY KEY NOT NULL,
|
||||
`games_played` integer DEFAULT 0 NOT NULL,
|
||||
`total_wins` integer DEFAULT 0 NOT NULL,
|
||||
`total_losses` integer DEFAULT 0 NOT NULL,
|
||||
`best_time` integer,
|
||||
`highest_accuracy` real DEFAULT 0 NOT NULL,
|
||||
`favorite_game_type` text,
|
||||
`game_stats` text DEFAULT '{}' NOT NULL,
|
||||
`last_played_at` integer,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
|
||||
-- Create worksheet_settings table for persisting user worksheet generator preferences
|
||||
CREATE TABLE `worksheet_settings` (
|
||||
`id` TEXT PRIMARY KEY NOT NULL,
|
||||
`user_id` TEXT NOT NULL,
|
||||
`worksheet_type` TEXT NOT NULL,
|
||||
`config` TEXT NOT NULL,
|
||||
`created_at` INTEGER NOT NULL,
|
||||
`updated_at` INTEGER NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Create index for efficient lookups by user and worksheet type
|
||||
CREATE INDEX `worksheet_settings_user_type_idx` ON `worksheet_settings` (`user_id`, `worksheet_type`);
|
||||
@@ -1,28 +0,0 @@
|
||||
-- Custom SQL migration file, put your code below! --
|
||||
|
||||
-- Remove foreign key constraint from worksheet_settings to allow guest users
|
||||
-- SQLite doesn't support DROP CONSTRAINT, so we need to recreate the table
|
||||
|
||||
-- Create new table without foreign key
|
||||
CREATE TABLE `worksheet_settings_new` (
|
||||
`id` TEXT PRIMARY KEY NOT NULL,
|
||||
`user_id` TEXT NOT NULL,
|
||||
`worksheet_type` TEXT NOT NULL,
|
||||
`config` TEXT NOT NULL,
|
||||
`created_at` INTEGER NOT NULL,
|
||||
`updated_at` INTEGER NOT NULL
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Copy existing data (if any)
|
||||
INSERT INTO `worksheet_settings_new`
|
||||
SELECT id, user_id, worksheet_type, config, created_at, updated_at
|
||||
FROM `worksheet_settings`;--> statement-breakpoint
|
||||
|
||||
-- Drop old table
|
||||
DROP TABLE `worksheet_settings`;--> statement-breakpoint
|
||||
|
||||
-- Rename new table to original name
|
||||
ALTER TABLE `worksheet_settings_new` RENAME TO `worksheet_settings`;--> statement-breakpoint
|
||||
|
||||
-- Recreate index
|
||||
CREATE INDEX `worksheet_settings_user_type_idx` ON `worksheet_settings` (`user_id`, `worksheet_type`);
|
||||
@@ -85,27 +85,6 @@
|
||||
"when": 1760800000000,
|
||||
"tag": "0011_add_room_game_configs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1761939039939,
|
||||
"tag": "0012_damp_mongoose",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "6",
|
||||
"when": 1762432185673,
|
||||
"tag": "0013_conscious_firebird",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "6",
|
||||
"when": 1762434916279,
|
||||
"tag": "0014_remarkable_master_chief",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
const createNextIntlPlugin = require('next-intl/plugin')
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
eslint: {
|
||||
@@ -67,4 +63,4 @@ const nextConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = withNextIntl(nextConfig)
|
||||
module.exports = nextConfig
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && concurrently \"node server.js\" \"npx @pandacss/dev --watch\"",
|
||||
"build": "node scripts/generate-build-info.js && npx @pandacss/dev && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && next build",
|
||||
"build": "node scripts/generate-build-info.js && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && next build",
|
||||
"start": "NODE_ENV=production node server.js",
|
||||
"lint": "npx @biomejs/biome lint . && npx eslint .",
|
||||
"lint:fix": "npx @biomejs/biome lint . --write && npx eslint . --fix",
|
||||
@@ -46,9 +46,7 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-spring/web": "^10.0.3",
|
||||
"@react-three/drei": "^9.117.0",
|
||||
"@react-three/fiber": "^8.17.0",
|
||||
"@react-spring/web": "^10.0.2",
|
||||
"@soroban/abacus-react": "workspace:*",
|
||||
"@soroban/core": "workspace:*",
|
||||
"@soroban/templates": "workspace:*",
|
||||
@@ -59,32 +57,20 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"emojibase-data": "^16.0.3",
|
||||
"jose": "^6.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lib0": "^0.2.114",
|
||||
"lucide-react": "^0.294.0",
|
||||
"make-plural": "^7.4.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "^14.2.32",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"next-intl": "^4.4.0",
|
||||
"openscad-wasm-prebuilt": "^1.2.0",
|
||||
"python-bridge": "^1.1.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-layout": "^0.7.3",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-textfit": "^1.1.1",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"three": "^0.169.0",
|
||||
"y-protocols": "^1.0.6",
|
||||
"y-websocket": "^3.0.0",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -100,8 +86,6 @@
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-textfit": "^1.1.4",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"concurrently": "^8.0.0",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// Inline version of abacus.scad that doesn't require BOSL2
|
||||
// This version uses a hardcoded bounding box size instead of the bounding_box() function
|
||||
|
||||
// ---- USER CUSTOMIZABLE PARAMETERS ----
|
||||
// These can be overridden via command line: -D 'columns=7' etc.
|
||||
columns = 13; // Total number of columns (1-13, mirrored book design)
|
||||
scale_factor = 1.5; // Overall size scale (preserves aspect ratio)
|
||||
// -----------------------------------------
|
||||
|
||||
stl_path = "/3d-models/simplified.abacus.stl";
|
||||
|
||||
// Known bounding box dimensions of the simplified.abacus.stl file
|
||||
// These were measured from the original file
|
||||
bbox_size = [186, 60, 120]; // [width, depth, height] in STL units
|
||||
|
||||
// Calculate parameters based on column count
|
||||
// The full STL has 13 columns. We want columns/2 per side (mirrored).
|
||||
total_columns_in_stl = 13;
|
||||
columns_per_side = columns / 2;
|
||||
width_scale = columns_per_side / total_columns_in_stl;
|
||||
|
||||
// Column spacing: distance between mirrored halves
|
||||
units_per_column = bbox_size[0] / total_columns_in_stl; // ~14.3 units per column
|
||||
column_spacing = columns_per_side * units_per_column;
|
||||
|
||||
// --- actual model ---
|
||||
module imported() {
|
||||
import(stl_path, convexity = 10);
|
||||
}
|
||||
|
||||
// Create a bounding box manually instead of using BOSL2's bounding_box()
|
||||
module bounding_box_manual() {
|
||||
translate([-bbox_size[0]/2, -bbox_size[1]/2, -bbox_size[2]/2])
|
||||
cube(bbox_size);
|
||||
}
|
||||
|
||||
module half_abacus() {
|
||||
intersection() {
|
||||
scale([width_scale, 1, 1]) bounding_box_manual();
|
||||
imported();
|
||||
}
|
||||
}
|
||||
|
||||
scale([scale_factor, scale_factor, scale_factor]) {
|
||||
translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus();
|
||||
half_abacus();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
include <BOSL2/std.scad>; // BOSL2 v2.0 or newer
|
||||
|
||||
// ---- USER CUSTOMIZABLE PARAMETERS ----
|
||||
// These can be overridden via command line: -D 'columns=7' etc.
|
||||
columns = 13; // Total number of columns (1-13, mirrored book design)
|
||||
scale_factor = 1.5; // Overall size scale (preserves aspect ratio)
|
||||
// -----------------------------------------
|
||||
|
||||
stl_path = "./simplified.abacus.stl";
|
||||
|
||||
// Calculate parameters based on column count
|
||||
// The full STL has 13 columns. We want columns/2 per side (mirrored).
|
||||
// The original bounding box intersection: scale([35/186, 1, 1])
|
||||
// 35/186 ≈ 0.188 = ~2.44 columns, so 186 units ≈ 13 columns, ~14.3 units per column
|
||||
total_columns_in_stl = 13;
|
||||
columns_per_side = columns / 2;
|
||||
width_scale = columns_per_side / total_columns_in_stl;
|
||||
|
||||
// Column spacing: distance between mirrored halves
|
||||
// Original spacing of 69 for ~2.4 columns/side
|
||||
// Calculate proportional spacing based on columns
|
||||
units_per_column = 186 / total_columns_in_stl; // ~14.3 units per column
|
||||
column_spacing = columns_per_side * units_per_column;
|
||||
|
||||
// --- actual model ---
|
||||
module imported()
|
||||
import(stl_path, convexity = 10);
|
||||
|
||||
module half_abacus() {
|
||||
intersection() {
|
||||
scale([width_scale, 1, 1]) bounding_box() imported();
|
||||
imported();
|
||||
}
|
||||
}
|
||||
|
||||
scale([scale_factor, scale_factor, scale_factor]) {
|
||||
translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus();
|
||||
half_abacus();
|
||||
}
|
||||
@@ -1,581 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
|
||||
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
|
||||
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
|
||||
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
|
||||
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
|
||||
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
|
||||
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 69 KiB |
@@ -1,896 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(24.400000000000023 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(24.400000000000023 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(24.400000000000023 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(24.400000000000023 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
|
||||
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
|
||||
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
|
||||
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 98 KiB |
@@ -1,578 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE52AD68C7B06D7D319E57C0DFEC4A716" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
|
||||
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
|
||||
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
|
||||
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
|
||||
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE52AD68C7B06D7D319E57C0DFEC4A716" overflow="visible">
|
||||
<path d="M 28.791 10.584001 C 28.791 12.852 28.098 15.687 25.704 18.333 C 24.507 19.656 23.499 20.286001 19.467001 22.806 C 24.003 25.137001 27.09 28.413 27.09 32.571 C 27.09 38.367 21.483 41.958 15.75 41.958 C 9.45 41.958 4.347 37.296 4.347 31.437 C 4.347 30.303001 4.473 27.468 7.119 24.507 C 7.8120003 23.751 10.143001 22.176 11.718 21.105 C 8.064 19.278 2.6460001 15.75 2.6460001 9.5130005 C 2.6460001 2.835 9.0720005 -1.386 15.687 -1.386 C 22.806 -1.386 28.791 3.8430002 28.791 10.584001 Z M 24.318 32.571 C 24.318 28.98 21.861 25.956001 18.081 23.751 L 10.269 28.791 C 7.3710003 30.681 7.119 32.823 7.119 33.894 C 7.119 37.737 11.214 40.383 15.687 40.383 C 20.286001 40.383 24.318 37.107002 24.318 32.571 Z M 25.641 8.316 C 25.641 3.654 20.916 0.37800002 15.75 0.37800002 C 10.332 0.37800002 5.796 4.284 5.796 9.5130005 C 5.796 13.167 7.8120003 17.199 13.167 20.16 L 20.916 15.246 C 22.68 14.049 25.641 12.159 25.641 8.316 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 68 KiB |
@@ -1,899 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 27.846500000000002)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g7C2A22CF0123BA7341054F23FB3AEE5D" x="34.009" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 73.02324999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(53.949999999999996 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20.4 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(11.1503 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6BF4B3C6EAE864CD7BB10E0691834549" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g5DFA18D209ACD12A0DB62DA18B290FC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 27.846500000000002)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3EB0DD94D29D91EA51A9F1A322CF0975" x="34.009" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 73.02324999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(53.949999999999996 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g2FBAD5FC5745664B6ADE5AD1F11D2E13" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g13CC2BF7150CAD2A56D6E42C1E1AA71A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20.4 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(11.1503 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g2FBAD5FC5745664B6ADE5AD1F11D2E13" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6BF4B3C6EAE864CD7BB10E0691834549" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 27.846500000000002)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA12A273C0EA9CCB93CDAAF5C3476B99" x="34.009" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 73.02324999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(53.949999999999996 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g2FBAD5FC5745664B6ADE5AD1F11D2E13" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g13CC2BF7150CAD2A56D6E42C1E1AA71A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20.4 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(11.1503 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F9EB82CA73B0CEFA6A6BBB3DF89DD91" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g5DFA18D209ACD12A0DB62DA18B290FC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 27.846500000000002)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB3094794D0628B160CB8E5B1E6D90854" x="34.009" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 73.02324999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(53.949999999999996 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 59.1)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F9EB82CA73B0CEFA6A6BBB3DF89DD91" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20.4 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(11.1503 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 118.2)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(17.724999999999998 45.702949999999994)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g13CC2BF7150CAD2A56D6E42C1E1AA71A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
|
||||
</g>
|
||||
<g transform="translate(53.949999999999996 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(113.05 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(172.15000000000003 177.29999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB0D702D3F5E8AA5BEC3D8B58F45868E0" overflow="visible">
|
||||
<path d="M 31.701502 4.9345 C 31.701502 5.9995003 30.672 5.9995003 30.246 5.9995003 L 21.797 5.9995003 L 23.5365 11.786 L 30.246 11.786 C 30.672 11.786 31.701502 11.786 31.701502 12.851001 C 31.701502 13.951501 30.601002 13.951501 30.0685 13.951501 L 24.2465 13.951501 L 27.015501 22.862001 C 27.157501 23.288 27.157501 23.323502 27.157501 23.572 C 27.157501 24.104502 26.7315 24.637001 26.057001 24.637001 C 25.276001 24.637001 25.098501 24.0335 24.956501 23.572 L 21.9745 13.951501 L 15.158501 13.951501 L 17.9275 22.862001 C 18.0695 23.288 18.0695 23.323502 18.0695 23.572 C 18.0695 24.104502 17.643501 24.637001 16.969 24.637001 C 16.188 24.637001 16.0105 24.0335 15.868501 23.572 L 12.8865 13.951501 L 3.9050002 13.951501 C 3.3725002 13.951501 2.272 13.951501 2.272 12.851001 C 2.272 11.786 3.3015 11.786 3.7275002 11.786 L 12.1765 11.786 L 10.437 5.9995003 L 3.7275002 5.9995003 C 3.3015 5.9995003 2.272 5.9995003 2.272 4.9345 C 2.272 3.834 3.3725002 3.834 3.9050002 3.834 L 9.727 3.834 L 6.958 -5.112 C 6.887 -5.3250003 6.816 -5.538 6.816 -5.822 C 6.816 -6.3545003 7.242 -6.887 7.9165 -6.887 C 8.662001 -6.887 8.8395 -6.3190002 8.981501 -5.893 L 11.999001 3.834 L 18.815 3.834 L 16.046001 -5.112 C 15.975 -5.3250003 15.904 -5.538 15.904 -5.822 C 15.904 -6.3545003 16.33 -6.887 17.004501 -6.887 C 17.75 -6.887 17.9275 -6.3190002 18.0695 -5.893 L 21.087 3.834 L 30.0685 3.834 C 30.601002 3.834 31.701502 3.834 31.701502 4.9345 Z M 21.264502 11.786 L 19.525002 5.9995003 L 12.709001 5.9995003 L 14.448501 11.786 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g7C2A22CF0123BA7341054F23FB3AEE5D" overflow="visible">
|
||||
<path d="M 17.537 0 L 17.537 1.6685001 L 12.567 1.6685001 L 12.567 22.1165 C 12.567 22.897501 12.567 23.2525 11.644 23.2525 C 11.2535 23.2525 11.182501 23.2525 10.863 23.004002 C 8.1295 20.980501 4.473 20.980501 3.7275002 20.980501 L 3.0175002 20.980501 L 3.0175002 19.312 L 3.7275002 19.312 C 4.2955003 19.312 6.248 19.3475 8.342501 20.022001 L 8.342501 1.6685001 L 3.408 1.6685001 L 3.408 0 C 4.9700003 0.1065 8.733001 0.1065 10.472501 0.1065 C 12.212001 0.1065 15.975 0.1065 17.537 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gD9DD5739D64F483B0A1931A9965DFBC1" overflow="visible">
|
||||
<path d="M 8.4135 2.769 C 8.4135 4.2955003 7.171 5.538 5.6445003 5.538 C 4.118 5.538 2.8755002 4.2955003 2.8755002 2.769 C 2.8755002 1.2425001 4.118 0 5.6445003 0 C 7.171 0 8.4135 1.2425001 8.4135 2.769 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEBD20B78B8E26035E6B0027C46547B5B" overflow="visible">
|
||||
<path d="M 21.2377 8.2302 L 20.0552 8.2302 C 19.8187 6.8112 19.4876 4.73 19.0146 4.0205 C 18.6835 3.6421 15.5617 3.6421 14.5211 3.6421 L 6.0071 3.6421 L 11.0209 8.514 C 18.3997 15.0414 21.2377 17.5956 21.2377 22.3256 C 21.2377 27.7178 16.9807 31.5018 11.2101 31.5018 C 5.8652 31.5018 2.365 27.1502 2.365 22.9405 C 2.365 20.2917 4.73 20.2917 4.8719 20.2917 C 5.676 20.2917 7.3315 20.8593 7.3315 22.7986 C 7.3315 24.0284 6.4801 25.2582 4.8245997 25.2582 C 4.4462 25.2582 4.3516 25.2582 4.2097 25.2109 C 5.2976 28.2854 7.8518 30.0355 10.5952 30.0355 C 14.8995 30.0355 16.9334 26.2042 16.9334 22.3256 C 16.9334 18.5416 14.5684 14.8049 11.9669 11.8723 L 2.8853 1.7501 C 2.365 1.2298 2.365 1.1352 2.365 0 L 19.9133 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g702B42440DB7C8CF270F99AC35604E" overflow="visible">
|
||||
<path d="M 34.1506 11.825 C 34.1506 12.3453 33.7249 12.771 33.2046 12.771 L 19.3457 12.771 L 19.3457 26.6299 C 19.3457 27.1502 18.92 27.5759 18.3997 27.5759 C 17.8794 27.5759 17.4537 27.1502 17.4537 26.6299 L 17.4537 12.771 L 3.5948 12.771 C 3.0745 12.771 2.6488 12.3453 2.6488 11.825 C 2.6488 11.3047 3.0745 10.879 3.5948 10.879 L 17.4537 10.879 L 17.4537 -2.9799 C 17.4537 -3.5002 17.8794 -3.9259 18.3997 -3.9259 C 18.92 -3.9259 19.3457 -3.5002 19.3457 -2.9799 L 19.3457 10.879 L 33.2046 10.879 C 33.7249 10.879 34.1506 11.3047 34.1506 11.825 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6BF4B3C6EAE864CD7BB10E0691834549" overflow="visible">
|
||||
<path d="M 22.2783 7.8045 L 22.2783 9.2708 L 17.5483 9.2708 L 17.5483 30.7923 C 17.5483 31.7383 17.5483 32.0221 16.7915 32.0221 C 16.3658 32.0221 16.2239 32.0221 15.8455 31.4545 L 1.3244 9.2708 L 1.3244 7.8045 L 13.906199 7.8045 L 13.906199 3.6894 C 13.906199 1.9866 13.8116 1.4663 10.3114 1.4663 L 9.3181 1.4663 L 9.3181 0 C 11.2574 0.1419 13.717 0.1419 15.7036 0.1419 C 17.6902 0.1419 20.1971 0.1419 22.1364 0 L 22.1364 1.4663 L 21.1431 1.4663 C 17.6429 1.4663 17.5483 1.9866 17.5483 3.6894 L 17.5483 7.8045 Z M 14.19 9.2708 L 2.6488 9.2708 L 14.19 26.9137 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g5DFA18D209ACD12A0DB62DA18B290FC" overflow="visible">
|
||||
<path d="M 21.6161 9.6491995 C 21.6161 15.6563 17.4064 20.1971 12.1561 20.1971 C 8.9397 20.1971 7.1896 17.7848 6.2436 15.5144 L 6.2436 16.6496 C 6.2436 28.616499 12.1088 30.3193 14.5211 30.3193 C 15.6563 30.3193 17.6429 30.0355 18.6835 28.4273 C 17.973999 28.4273 16.082 28.4273 16.082 26.2988 C 16.082 24.8325 17.2172 24.123 18.2578 24.123 C 19.0146 24.123 20.4336 24.5487 20.4336 26.3934 C 20.4336 29.2314 18.3524 31.5018 14.4265 31.5018 C 8.3721 31.5018 1.9866 25.4001 1.9866 14.9468 C 1.9866 2.3177 7.4734 -1.0406 11.8723 -1.0406 C 17.1226 -1.0406 21.6161 3.4056 21.6161 9.6491995 Z M 17.3591 9.6965 C 17.3591 7.4261 17.3591 5.0611 16.555 3.3583 C 15.136 0.5203 12.9602 0.2838 11.8723 0.2838 C 8.8924 0.2838 7.4734 3.1218 7.1896 3.8313 C 6.3382 6.0544 6.3382 9.8384 6.3382 10.6898 C 6.3382 14.3792 7.8518 19.1092 12.1088 19.1092 C 12.8656 19.1092 15.0414 19.1092 16.5077 16.1766 C 17.3591 14.4265 17.3591 12.0142 17.3591 9.6965 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3EB0DD94D29D91EA51A9F1A322CF0975" overflow="visible">
|
||||
<path d="M 18.3535 7.881 L 16.685001 7.881 C 16.5785 7.171 16.259 4.899 15.762 4.6505003 C 15.371501 4.4375 12.709001 4.4375 12.141001 4.4375 L 6.9225 4.4375 C 8.591001 5.822 10.437 7.3485003 12.0345 8.52 C 16.081501 11.502001 18.3535 13.170501 18.3535 16.543001 C 18.3535 20.6255 14.661501 23.2525 9.656 23.2525 C 5.3605003 23.2525 2.0235 21.0515 2.0235 17.8565 C 2.0235 15.762 3.7275002 15.229501 4.5795 15.229501 C 5.7155004 15.229501 7.1355004 16.0105 7.1355004 17.785501 C 7.1355004 19.6315 5.6445003 20.199501 5.112 20.341501 C 6.1415 21.1935 7.4905005 21.584002 8.7685 21.584002 C 11.821501 21.584002 13.4545 19.17 13.4545 16.5075 C 13.4545 14.058001 12.1055 11.644 9.620501 9.1235 L 2.4850001 1.8460001 C 2.0235 1.4200001 2.0235 1.3490001 2.0235 0.639 L 2.0235 0 L 17.253 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g2FBAD5FC5745664B6ADE5AD1F11D2E13" overflow="visible">
|
||||
<path d="M 19.8187 0 L 19.8187 1.4663 L 18.3051 1.4663 C 14.0480995 1.4663 13.906199 1.9866 13.906199 3.7367 L 13.906199 30.272 C 13.906199 31.4072 13.906199 31.5018 12.8183 31.5018 C 9.8857 28.4746 5.7233 28.4746 4.2097 28.4746 L 4.2097 27.0083 C 5.1557 27.0083 7.9464 27.0083 10.406 28.2381 L 10.406 3.7367 C 10.406 2.0339 10.2641 1.4663 6.0071 1.4663 L 4.4934998 1.4663 L 4.4934998 0 C 6.149 0.1419 10.2641 0.1419 12.1561 0.1419 C 14.0480995 0.1419 18.1632 0.1419 19.8187 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g13CC2BF7150CAD2A56D6E42C1E1AA71A" overflow="visible">
|
||||
<path d="M 21.758 15.136 C 21.758 18.92 21.5215 22.704 19.866 26.2042 C 17.6902 30.744999 13.8116 31.5018 11.825 31.5018 C 8.9869995 31.5018 5.5341 30.272 3.5948 25.8731 C 2.0812 22.6094 1.8447 18.92 1.8447 15.136 C 1.8447 11.5885 2.0339 7.3315 3.9732 3.7367 C 6.0071 -0.0946 9.46 -1.0406 11.777699 -1.0406 C 14.3319 -1.0406 17.9267 -0.0473 20.0079 4.4462 C 21.5215 7.7099 21.758 11.3993 21.758 15.136 Z M 17.8321 15.7036 C 17.8321 12.1561 17.8321 8.9397 17.3118 5.9125 C 16.6023 1.419 13.906199 0 11.777699 0 C 9.933 0 7.1423 1.1825 6.2908998 5.7233 C 5.7706 8.5613 5.7706 12.9129 5.7706 15.7036 C 5.7706 18.7308 5.7706 21.8526 6.149 24.4068 C 7.0477 30.0355 10.5952 30.4612 11.777699 30.4612 C 13.3386 30.4612 16.4604 29.6098 17.3591 24.9271 C 17.8321 22.2783 17.8321 18.6835 17.8321 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA12A273C0EA9CCB93CDAAF5C3476B99" overflow="visible">
|
||||
<path d="M 18.673 6.3900003 C 18.673 8.342501 17.643501 11.3955 12.496 12.496 C 14.9455 13.241501 17.537 15.265 17.537 18.318 C 17.537 21.0515 14.839001 23.2525 9.8335 23.2525 C 5.609 23.2525 2.8400002 20.980501 2.8400002 18.140501 C 2.8400002 16.614 3.9405 15.6555 5.2895 15.6555 C 6.887 15.6555 7.7745004 16.7915 7.7745004 18.105001 C 7.7745004 20.164001 5.8575 20.5545 5.7155004 20.59 C 6.958 21.584002 8.52 21.868 9.620501 21.868 C 12.567 21.868 12.6735 19.596 12.6735 18.424501 C 12.6735 17.963001 12.6380005 13.3125 8.946 13.099501 C 7.4905005 13.028501 7.4195004 12.993 7.242 12.9575 C 6.887 12.922001 6.816 12.567 6.816 12.354 C 6.816 11.715 7.171 11.715 7.8100004 11.715 L 9.372001 11.715 C 13.241501 11.715 13.241501 8.236 13.241501 6.4255004 C 13.241501 4.757 13.241501 1.136 9.5495 1.136 C 8.6265 1.136 6.7805004 1.278 5.0765 2.3430002 C 6.248 2.6625001 7.1355004 3.5500002 7.1355004 5.0055003 C 7.1355004 6.603 5.9995003 7.7035003 4.4375 7.7035003 C 2.9465 7.7035003 1.704 6.7450004 1.704 4.9345 C 1.704 1.7395 5.1475 -0.3905 9.727 -0.3905 C 16.081501 -0.3905 18.673 3.1595001 18.673 6.3900003 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3F9EB82CA73B0CEFA6A6BBB3DF89DD91" overflow="visible">
|
||||
<path d="M 21.6161 7.9464 C 21.6161 9.6491995 21.0958 11.777699 19.298399 13.7643 C 18.3997 14.7576 17.6429 15.2306 14.6157 17.1226 C 18.0213 18.8727 20.339 21.3323 20.339 24.4541 C 20.339 28.8057 16.1293 31.5018 11.825 31.5018 C 7.095 31.5018 3.2637 28.0016 3.2637 23.6027 C 3.2637 22.7513 3.3583 20.6228 5.3449 18.3997 C 5.8652 17.8321 7.6153 16.6496 8.7978 15.8455 C 6.0544 14.4738 1.9866 11.825 1.9866 7.1423 C 1.9866 2.1285 6.8112 -1.0406 11.777699 -1.0406 C 17.1226 -1.0406 21.6161 2.8853 21.6161 7.9464 Z M 18.2578 24.4541 C 18.2578 21.758 16.4131 19.4876 13.5751 17.8321 L 7.7099 21.6161 C 5.5341 23.035099 5.3449 24.6433 5.3449 25.4474 C 5.3449 28.3327 8.4194 30.3193 11.777699 30.3193 C 15.2306 30.3193 18.2578 27.8597 18.2578 24.4541 Z M 19.2511 6.2436 C 19.2511 2.7434 15.7036 0.2838 11.825 0.2838 C 7.7572 0.2838 4.3516 3.2164 4.3516 7.1423 C 4.3516 9.8857 5.8652 12.9129 9.8857 15.136 L 15.7036 11.4466 C 17.028 10.5479 19.2511 9.1289 19.2511 6.2436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB3094794D0628B160CB8E5B1E6D90854" overflow="visible">
|
||||
<path d="M 19.241001 0 L 19.241001 1.6685001 L 15.797501 1.6685001 L 15.797501 5.538 L 19.241001 5.538 L 19.241001 7.2065 L 15.797501 7.2065 L 15.797501 22.152 C 15.797501 23.1105 15.7265005 23.288 14.7325 23.288 C 13.987 23.288 13.951501 23.2525 13.5255 22.720001 L 1.136 7.2065 L 1.136 5.538 L 11.360001 5.538 L 11.360001 1.6685001 L 7.3840003 1.6685001 L 7.3840003 0 C 8.733001 0.1065 11.9635 0.1065 13.490001 0.1065 C 14.910001 0.1065 17.9985 0.1065 19.241001 0 Z M 11.750501 7.2065 L 2.982 7.2065 L 11.750501 18.2115 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 99 KiB |
@@ -1,578 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE52AD68C7B06D7D319E57C0DFEC4A716" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
|
||||
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE52AD68C7B06D7D319E57C0DFEC4A716" overflow="visible">
|
||||
<path d="M 28.791 10.584001 C 28.791 12.852 28.098 15.687 25.704 18.333 C 24.507 19.656 23.499 20.286001 19.467001 22.806 C 24.003 25.137001 27.09 28.413 27.09 32.571 C 27.09 38.367 21.483 41.958 15.75 41.958 C 9.45 41.958 4.347 37.296 4.347 31.437 C 4.347 30.303001 4.473 27.468 7.119 24.507 C 7.8120003 23.751 10.143001 22.176 11.718 21.105 C 8.064 19.278 2.6460001 15.75 2.6460001 9.5130005 C 2.6460001 2.835 9.0720005 -1.386 15.687 -1.386 C 22.806 -1.386 28.791 3.8430002 28.791 10.584001 Z M 24.318 32.571 C 24.318 28.98 21.861 25.956001 18.081 23.751 L 10.269 28.791 C 7.3710003 30.681 7.119 32.823 7.119 33.894 C 7.119 37.737 11.214 40.383 15.687 40.383 C 20.286001 40.383 24.318 37.107002 24.318 32.571 Z M 25.641 8.316 C 25.641 3.654 20.916 0.37800002 15.75 0.37800002 C 10.332 0.37800002 5.796 4.284 5.796 9.5130005 C 5.796 13.167 7.8120003 17.199 13.167 20.16 L 20.916 15.246 C 22.68 14.049 25.641 12.159 25.641 8.316 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
|
||||
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
|
||||
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
|
||||
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 69 KiB |
@@ -1,686 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
|
||||
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
|
||||
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
|
||||
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
|
||||
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
|
||||
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 79 KiB |
@@ -1,575 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
|
||||
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
|
||||
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
|
||||
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
|
||||
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 67 KiB |
@@ -1,578 +0,0 @@
|
||||
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
|
||||
<g>
|
||||
<g transform="translate(28.800000000000004 28.800000000000004)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 8.196)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(472.3544 7.1032)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE52AD68C7B06D7D319E57C0DFEC4A716" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 34.596)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(277.2 389.196)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(0 0)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(5.76 35.905899999999995)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
|
||||
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(0 37.65295000000001)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(103.20000000000002 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 78.79999999999998)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(-18.99999999999997 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(14.892999999999994 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(182 157.59999999999997)">
|
||||
<g class="typst-group">
|
||||
<g>
|
||||
<g transform="translate(-0 -0)">
|
||||
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
|
||||
</g>
|
||||
<g transform="translate(23.649999999999995 60.9145)">
|
||||
<g class="typst-text" transform="scale(1, -1)">
|
||||
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(24.400000000000023 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(103.20000000000002 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
<g transform="translate(182 236.39999999999998)">
|
||||
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs id="glyph">
|
||||
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
|
||||
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
|
||||
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
|
||||
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
|
||||
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
|
||||
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
|
||||
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
|
||||
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
|
||||
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
|
||||
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
|
||||
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
|
||||
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
|
||||
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
|
||||
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
|
||||
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
|
||||
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
|
||||
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
|
||||
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
|
||||
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
|
||||
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
|
||||
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
|
||||
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE52AD68C7B06D7D319E57C0DFEC4A716" overflow="visible">
|
||||
<path d="M 28.791 10.584001 C 28.791 12.852 28.098 15.687 25.704 18.333 C 24.507 19.656 23.499 20.286001 19.467001 22.806 C 24.003 25.137001 27.09 28.413 27.09 32.571 C 27.09 38.367 21.483 41.958 15.75 41.958 C 9.45 41.958 4.347 37.296 4.347 31.437 C 4.347 30.303001 4.473 27.468 7.119 24.507 C 7.8120003 23.751 10.143001 22.176 11.718 21.105 C 8.064 19.278 2.6460001 15.75 2.6460001 9.5130005 C 2.6460001 2.835 9.0720005 -1.386 15.687 -1.386 C 22.806 -1.386 28.791 3.8430002 28.791 10.584001 Z M 24.318 32.571 C 24.318 28.98 21.861 25.956001 18.081 23.751 L 10.269 28.791 C 7.3710003 30.681 7.119 32.823 7.119 33.894 C 7.119 37.737 11.214 40.383 15.687 40.383 C 20.286001 40.383 24.318 37.107002 24.318 32.571 Z M 25.641 8.316 C 25.641 3.654 20.916 0.37800002 15.75 0.37800002 C 10.332 0.37800002 5.796 4.284 5.796 9.5130005 C 5.796 13.167 7.8120003 17.199 13.167 20.16 L 20.916 15.246 C 22.68 14.049 25.641 12.159 25.641 8.316 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
|
||||
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
|
||||
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
|
||||
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
|
||||
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
|
||||
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
|
||||
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
|
||||
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
|
||||
</symbol>
|
||||
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
|
||||
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
|
||||
</symbol>
|
||||
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
|
||||
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 69 KiB |
@@ -398,8 +398,8 @@
|
||||
<!-- Import Typst.ts and SVG Processor -->
|
||||
<script>
|
||||
// Global variables
|
||||
const typstRenderer = null;
|
||||
const flashcardsTemplate = null;
|
||||
let typstRenderer = null;
|
||||
let flashcardsTemplate = null;
|
||||
|
||||
// Initialize everything - use web app's API instead of direct Typst
|
||||
async function initialize() {
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,220 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Generate icon.svg and og-image.svg from AbacusReact component
|
||||
*
|
||||
* This script renders AbacusReact server-side to produce the exact same
|
||||
* SVG output as the interactive client-side version (without animations).
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
|
||||
// Extract just the SVG element content from rendered output
|
||||
function extractSvgContent(markup: string): string {
|
||||
// Find the opening <svg and closing </svg> tags
|
||||
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
|
||||
if (!svgMatch) {
|
||||
throw new Error('No SVG element found in rendered output')
|
||||
}
|
||||
return svgMatch[1] // Return just the inner content
|
||||
}
|
||||
|
||||
// Generate the favicon (icon.svg) - single column showing value 5
|
||||
function generateFavicon(): string {
|
||||
const abacusMarkup = renderToStaticMarkup(
|
||||
<AbacusReact
|
||||
value={5}
|
||||
columns={1}
|
||||
scaleFactor={1.0}
|
||||
animated={false}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
customStyles={{
|
||||
heavenBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
|
||||
columnPosts: {
|
||||
fill: '#451a03',
|
||||
stroke: '#292524',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: '#292524',
|
||||
stroke: '#292524',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract just the SVG content (without div wrapper)
|
||||
let svgContent = extractSvgContent(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
svgContent = svgContent.replace(/\s*!important/g, '')
|
||||
|
||||
// Wrap in SVG with proper viewBox for favicon sizing
|
||||
// AbacusReact with 1 column + scaleFactor 1.0 = ~25×120px
|
||||
// Scale 0.7 = ~17.5×84px, centered in 100×100
|
||||
return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle for better visibility -->
|
||||
<circle cx="50" cy="50" r="48" fill="#fef3c7"/>
|
||||
|
||||
<!-- Abacus from @soroban/abacus-react -->
|
||||
<g transform="translate(41, 8) scale(0.7)">
|
||||
${svgContent}
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
// Generate the Open Graph image (og-image.svg)
|
||||
function generateOGImage(): string {
|
||||
const abacusMarkup = renderToStaticMarkup(
|
||||
<AbacusReact
|
||||
value={1234}
|
||||
columns={4}
|
||||
scaleFactor={3.5}
|
||||
animated={false}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
customStyles={{
|
||||
columnPosts: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
columns: {
|
||||
0: {
|
||||
// Ones place (rightmost) - Blue
|
||||
heavenBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
|
||||
},
|
||||
1: {
|
||||
// Tens place - Green
|
||||
heavenBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
|
||||
},
|
||||
2: {
|
||||
// Hundreds place - Yellow/Gold
|
||||
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
|
||||
},
|
||||
3: {
|
||||
// Thousands place (leftmost) - Purple
|
||||
heavenBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract just the SVG content (without div wrapper)
|
||||
let svgContent = extractSvgContent(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
svgContent = svgContent.replace(/\s*!important/g, '')
|
||||
|
||||
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Dark background like homepage -->
|
||||
<rect width="1200" height="630" fill="#111827"/>
|
||||
|
||||
<!-- Subtle dot pattern background -->
|
||||
<defs>
|
||||
<pattern id="dots" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<circle cx="2" cy="2" r="1" fill="rgba(255, 255, 255, 0.15)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="1200" height="630" fill="url(#dots)" opacity="0.1"/>
|
||||
|
||||
<!-- Left decorative elements - Diamond shapes and math operators -->
|
||||
<g opacity="0.4">
|
||||
<!-- Purple diamond (thousands) -->
|
||||
<polygon points="150,120 180,150 150,180 120,150" fill="#c084fc" />
|
||||
<!-- Gold diamond (hundreds) -->
|
||||
<polygon points="150,220 180,250 150,280 120,250" fill="#fbbf24" />
|
||||
<!-- Green diamond (tens) -->
|
||||
<polygon points="150,320 180,350 150,380 120,350" fill="#4ade80" />
|
||||
<!-- Blue diamond (ones) -->
|
||||
<polygon points="150,420 180,450 150,480 120,450" fill="#60a5fa" />
|
||||
</g>
|
||||
|
||||
<!-- Left math operators -->
|
||||
<g opacity="0.35" fill="rgba(255, 255, 255, 0.8)">
|
||||
<text x="80" y="100" font-family="Arial, sans-serif" font-size="42" font-weight="300">+</text>
|
||||
<text x="240" y="190" font-family="Arial, sans-serif" font-size="42" font-weight="300">×</text>
|
||||
<text x="70" y="290" font-family="Arial, sans-serif" font-size="42" font-weight="300">=</text>
|
||||
<text x="250" y="390" font-family="Arial, sans-serif" font-size="42" font-weight="300">−</text>
|
||||
</g>
|
||||
|
||||
<!-- Right decorative elements - Diamond shapes and math operators -->
|
||||
<g opacity="0.4">
|
||||
<!-- Purple diamond (thousands) -->
|
||||
<polygon points="1050,120 1080,150 1050,180 1020,150" fill="#c084fc" />
|
||||
<!-- Gold diamond (hundreds) -->
|
||||
<polygon points="1050,220 1080,250 1050,280 1020,250" fill="#fbbf24" />
|
||||
<!-- Green diamond (tens) -->
|
||||
<polygon points="1050,320 1080,350 1050,380 1020,350" fill="#4ade80" />
|
||||
<!-- Blue diamond (ones) -->
|
||||
<polygon points="1050,420 1080,450 1050,480 1020,450" fill="#60a5fa" />
|
||||
</g>
|
||||
|
||||
<!-- Right math operators -->
|
||||
<g opacity="0.35" fill="rgba(255, 255, 255, 0.8)">
|
||||
<text x="940" y="160" font-family="Arial, sans-serif" font-size="42" font-weight="300">÷</text>
|
||||
<text x="1110" y="270" font-family="Arial, sans-serif" font-size="42" font-weight="300">+</text>
|
||||
<text x="920" y="360" font-family="Arial, sans-serif" font-size="42" font-weight="300">×</text>
|
||||
<text x="1120" y="480" font-family="Arial, sans-serif" font-size="42" font-weight="300">=</text>
|
||||
</g>
|
||||
|
||||
<!-- Huge centered abacus from @soroban/abacus-react -->
|
||||
<!-- AbacusReact 4 columns @ scale 3.5: width ~350px, height ~420px -->
|
||||
<!-- Center horizontally: (1200-350)/2 = 425px -->
|
||||
<!-- Center vertically in upper portion: abacus middle at ~225px, so start at 225-210 = 15px -->
|
||||
<g transform="translate(425, 15)">
|
||||
${svgContent}
|
||||
</g>
|
||||
|
||||
<!-- Title at bottom, horizontally and vertically centered in lower portion -->
|
||||
<!-- Position at y=520 for vertical centering in bottom half -->
|
||||
<text x="600" y="520" font-family="Arial, sans-serif" font-size="72" font-weight="bold" fill="url(#title-gradient)" text-anchor="middle">
|
||||
Abaci One
|
||||
</text>
|
||||
|
||||
<!-- Gold gradient for title -->
|
||||
<defs>
|
||||
<linearGradient id="title-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#fbbf24;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#f59e0b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#fbbf24;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const appDir = __dirname.replace('/scripts', '')
|
||||
|
||||
try {
|
||||
console.log('Generating Open Graph image from AbacusReact...')
|
||||
const ogImageSvg = generateOGImage()
|
||||
writeFileSync(join(appDir, 'public', 'og-image.svg'), ogImageSvg)
|
||||
console.log('✓ Generated public/og-image.svg')
|
||||
|
||||
console.log('\n✅ Icon generated successfully!')
|
||||
console.log('\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx')
|
||||
console.log('which calls scripts/generateDayIcon.tsx as a subprocess.')
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Script to generate example worksheet images for the blog post
|
||||
// Shows different scaffolding levels for the 2D difficulty blog post
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
|
||||
import { DIFFICULTY_PROFILES } from '../src/app/create/worksheets/addition/difficultyProfiles'
|
||||
|
||||
// Output directory
|
||||
const outputDir = path.join(process.cwd(), 'public', 'blog', 'difficulty-examples')
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Generate examples with SAME regrouping level but different scaffolding
|
||||
// This clearly shows how scaffolding changes while keeping problem complexity constant
|
||||
const examples = [
|
||||
{
|
||||
name: 'full-scaffolding',
|
||||
filename: 'full-scaffolding.svg',
|
||||
description: 'Full Scaffolding: Maximum visual support',
|
||||
// Use medium regrouping with full scaffolding
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'always' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'medium-scaffolding',
|
||||
filename: 'medium-scaffolding.svg',
|
||||
description: 'Medium Scaffolding: Strategic support',
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: 'whenRegrouping' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'when3PlusDigits' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'minimal-scaffolding',
|
||||
filename: 'minimal-scaffolding.svg',
|
||||
description: 'Minimal Scaffolding: Carry boxes only',
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: 'whenMultipleRegroups' as const,
|
||||
answerBoxes: 'never' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no-scaffolding',
|
||||
filename: 'no-scaffolding.svg',
|
||||
description: 'No Scaffolding: Students work independently',
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'never' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
|
||||
console.log('Generating blog example worksheets...\n')
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`Generating ${example.description}...`)
|
||||
|
||||
const config = {
|
||||
pAllStart: example.config.pAllStart,
|
||||
pAnyStart: example.config.pAnyStart,
|
||||
displayRules: example.config.displayRules,
|
||||
problemsPerPage: 4,
|
||||
pages: 1,
|
||||
cols: 2,
|
||||
}
|
||||
|
||||
try {
|
||||
const result = generateWorksheetPreview(config)
|
||||
|
||||
if (!result.success || !result.pages || result.pages.length === 0) {
|
||||
console.error(`Failed to generate ${example.name}:`, result.error)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the first page's SVG
|
||||
const svg = result.pages[0]
|
||||
|
||||
// Save to file
|
||||
const outputPath = path.join(outputDir, example.filename)
|
||||
fs.writeFileSync(outputPath, svg, 'utf-8')
|
||||
|
||||
console.log(` ✓ Saved to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error generating ${example.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nDone! Example worksheets generated.')
|
||||
console.log(`\nFiles saved to: ${outputDir}`)
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Generate a single day-of-month favicon
|
||||
* Usage: npx tsx scripts/generateDayIcon.tsx <day>
|
||||
* Example: npx tsx scripts/generateDayIcon.tsx 15
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
// Extract just the SVG element from rendered output
|
||||
function extractSvgElement(markup: string): string {
|
||||
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
|
||||
if (!svgMatch) {
|
||||
throw new Error('No SVG element found in rendered output')
|
||||
}
|
||||
return svgMatch[0]
|
||||
}
|
||||
|
||||
// Get day from command line argument
|
||||
const day = parseInt(process.argv[2], 10)
|
||||
|
||||
if (!day || day < 1 || day > 31) {
|
||||
console.error('Usage: npx tsx scripts/generateDayIcon.tsx <day>')
|
||||
console.error('Example: npx tsx scripts/generateDayIcon.tsx 15')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Render 2-column abacus showing day of month
|
||||
// Using AbacusStatic for server-side rendering
|
||||
const abacusMarkup = renderToStaticMarkup(
|
||||
<AbacusStatic
|
||||
value={day}
|
||||
columns={2}
|
||||
scaleFactor={1.8}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={true}
|
||||
frameVisible={true}
|
||||
cropToActiveBeads={{
|
||||
padding: {
|
||||
top: 8,
|
||||
bottom: 2,
|
||||
left: 5,
|
||||
right: 5,
|
||||
},
|
||||
}}
|
||||
customStyles={{
|
||||
columnPosts: {
|
||||
fill: '#1c1917',
|
||||
stroke: '#0c0a09',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: '#1c1917',
|
||||
stroke: '#0c0a09',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
columns: {
|
||||
0: {
|
||||
// Ones place - Gold (royal theme)
|
||||
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
|
||||
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
|
||||
},
|
||||
1: {
|
||||
// Tens place - Purple (royal theme)
|
||||
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
|
||||
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract the cropped SVG
|
||||
let croppedSvg = extractSvgElement(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
|
||||
|
||||
// Parse width and height from the cropped SVG
|
||||
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
|
||||
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
|
||||
|
||||
if (!widthMatch || !heightMatch) {
|
||||
throw new Error('Could not parse dimensions from cropped SVG')
|
||||
}
|
||||
|
||||
const croppedWidth = parseFloat(widthMatch[1])
|
||||
const croppedHeight = parseFloat(heightMatch[1])
|
||||
|
||||
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
|
||||
const targetSize = 96
|
||||
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
|
||||
|
||||
// Center in 100x100 canvas
|
||||
const scaledWidth = croppedWidth * scale
|
||||
const scaledHeight = croppedHeight * scale
|
||||
const offsetX = (100 - scaledWidth) / 2
|
||||
const offsetY = (100 - scaledHeight) / 2
|
||||
|
||||
// Wrap in 100x100 SVG canvas for favicon
|
||||
// Extract viewBox from cropped SVG to preserve it
|
||||
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
|
||||
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
|
||||
|
||||
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
|
||||
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
|
||||
viewBox="${viewBox}">
|
||||
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
|
||||
</svg>
|
||||
</svg>
|
||||
`
|
||||
|
||||
// Output to stdout so parent process can capture it
|
||||
process.stdout.write(svg)
|
||||
@@ -1,206 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to parse the Rithmomachia board CSV and verify the layout.
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const csvPath = path.join(
|
||||
process.env.HOME,
|
||||
'Downloads',
|
||||
'rithmomachia board setup - Sheet1 (1).csv'
|
||||
)
|
||||
|
||||
function parseCSV(csvContent) {
|
||||
const lines = csvContent.trim().split('\n')
|
||||
const pieces = []
|
||||
|
||||
// Process in triplets (color, shape, number)
|
||||
for (let rankIndex = 0; rankIndex < 16; rankIndex++) {
|
||||
const colorRowIndex = rankIndex * 3
|
||||
const shapeRowIndex = rankIndex * 3 + 1
|
||||
const numberRowIndex = rankIndex * 3 + 2
|
||||
|
||||
if (numberRowIndex >= lines.length) break
|
||||
|
||||
const colorRow = lines[colorRowIndex].split(',')
|
||||
const shapeRow = lines[shapeRowIndex].split(',')
|
||||
const numberRow = lines[numberRowIndex].split(',')
|
||||
|
||||
// Process each column (8 total)
|
||||
for (let colIndex = 0; colIndex < 8; colIndex++) {
|
||||
const color = colorRow[colIndex]?.trim()
|
||||
const shape = shapeRow[colIndex]?.trim()
|
||||
const numberStr = numberRow[colIndex]?.trim()
|
||||
|
||||
// Skip empty cells (but allow empty number for Pyramids)
|
||||
if (!color || !shape) continue
|
||||
|
||||
// Map CSV position to game square
|
||||
// CSV column → game row (1-8)
|
||||
// CSV rank → game column (A-P)
|
||||
const gameRow = colIndex + 1 // CSV col 0 → row 1, col 7 → row 8
|
||||
const gameCol = String.fromCharCode(65 + rankIndex) // rank 0 → A, rank 15 → P
|
||||
const square = `${gameCol}${gameRow}`
|
||||
|
||||
// Parse color
|
||||
const pieceColor = color.toLowerCase() === 'black' ? 'B' : 'W'
|
||||
|
||||
// Parse type
|
||||
let pieceType
|
||||
const shapeLower = shape.toLowerCase()
|
||||
if (shapeLower === 'circle') pieceType = 'C'
|
||||
else if (shapeLower === 'triangle' || shapeLower === 'traingle')
|
||||
pieceType = 'T' // Handle typo
|
||||
else if (shapeLower === 'square') pieceType = 'S'
|
||||
else if (shapeLower === 'pyramid') pieceType = 'P'
|
||||
else {
|
||||
console.warn(`Unknown shape "${shape}" at ${square}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse value/pyramid faces
|
||||
if (pieceType === 'P') {
|
||||
// Pyramid - number cell should be empty, use default faces
|
||||
pieces.push({
|
||||
color: pieceColor,
|
||||
type: pieceType,
|
||||
pyramidFaces: pieceColor === 'B' ? [36, 25, 16, 4] : [64, 49, 36, 25],
|
||||
square,
|
||||
})
|
||||
} else {
|
||||
// Regular piece needs a number
|
||||
if (!numberStr) {
|
||||
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const value = parseInt(numberStr, 10)
|
||||
if (isNaN(value)) {
|
||||
console.warn(`Invalid number "${numberStr}" at ${square}`)
|
||||
continue
|
||||
}
|
||||
|
||||
pieces.push({
|
||||
color: pieceColor,
|
||||
type: pieceType,
|
||||
value,
|
||||
square,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pieces
|
||||
}
|
||||
|
||||
function generateBoardDisplay(pieces) {
|
||||
const lines = []
|
||||
|
||||
lines.push('\n=== Board Layout (Game Orientation) ===')
|
||||
lines.push('BLACK (top)\n')
|
||||
lines.push(
|
||||
' A B C D E F G H I J K L M N O P'
|
||||
)
|
||||
|
||||
for (let row = 8; row >= 1; row--) {
|
||||
let line = `${row} `
|
||||
for (let colCode = 65; colCode <= 80; colCode++) {
|
||||
const col = String.fromCharCode(colCode)
|
||||
const square = `${col}${row}`
|
||||
const piece = pieces.find((p) => p.square === square)
|
||||
|
||||
if (piece) {
|
||||
const val = piece.type === 'P' ? ' P' : piece.value.toString().padStart(3, ' ')
|
||||
line += ` ${piece.color}${piece.type}${val} `
|
||||
} else {
|
||||
line += ' ---- '
|
||||
}
|
||||
}
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
lines.push('\nWHITE (bottom)\n')
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generateColumnSummaries(pieces) {
|
||||
const lines = []
|
||||
|
||||
lines.push('\n=== Column-by-Column Summary ===\n')
|
||||
|
||||
for (let colCode = 65; colCode <= 80; colCode++) {
|
||||
const col = String.fromCharCode(colCode)
|
||||
const columnPieces = pieces
|
||||
.filter((p) => p.square[0] === col)
|
||||
.sort((a, b) => {
|
||||
const rowA = parseInt(a.square.substring(1))
|
||||
const rowB = parseInt(b.square.substring(1))
|
||||
return rowA - rowB
|
||||
})
|
||||
|
||||
if (columnPieces.length === 0) continue
|
||||
|
||||
const color = columnPieces[0].color === 'B' ? 'BLACK' : 'WHITE'
|
||||
lines.push(`Column ${col} (${color}):`)
|
||||
for (const piece of columnPieces) {
|
||||
const val = piece.type === 'P' ? 'P[36,25,16,4]' : piece.value
|
||||
lines.push(` ${piece.square}: ${piece.type}(${val})`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function countPieces(pieces) {
|
||||
const blackPieces = pieces.filter((p) => p.color === 'B')
|
||||
const whitePieces = pieces.filter((p) => p.color === 'W')
|
||||
|
||||
const countByType = (pieces) => {
|
||||
const counts = { C: 0, T: 0, S: 0, P: 0 }
|
||||
for (const p of pieces) counts[p.type]++
|
||||
return counts
|
||||
}
|
||||
|
||||
const blackCounts = countByType(blackPieces)
|
||||
const whiteCounts = countByType(whitePieces)
|
||||
|
||||
console.log('\n=== Piece Counts ===')
|
||||
console.log(
|
||||
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`
|
||||
)
|
||||
console.log(
|
||||
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`
|
||||
)
|
||||
}
|
||||
|
||||
// Main
|
||||
try {
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf-8')
|
||||
const pieces = parseCSV(csvContent)
|
||||
|
||||
console.log(`\nParsed ${pieces.length} pieces from CSV`)
|
||||
console.log(generateBoardDisplay(pieces))
|
||||
console.log(generateColumnSummaries(pieces))
|
||||
countPieces(pieces)
|
||||
|
||||
// Save parsed data
|
||||
const outputPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'arcade-games',
|
||||
'rithmomachia',
|
||||
'utils',
|
||||
'parsedBoard.json'
|
||||
)
|
||||
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2))
|
||||
console.log(`\n✅ Saved parsed board to: ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Google Classroom API Setup Script
|
||||
# This script automates GCP project setup from the command line
|
||||
#
|
||||
# Prerequisites:
|
||||
# - gcloud CLI installed (brew install google-cloud-sdk)
|
||||
# - Valid Google account
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/setup-google-classroom.sh
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Google Classroom API Setup${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
PROJECT_ID="soroban-abacus-$(date +%s)" # Unique project ID with timestamp
|
||||
PROJECT_NAME="Soroban Abacus Flashcards"
|
||||
BILLING_ACCOUNT="" # Will prompt user if needed
|
||||
REDIRECT_URIS="http://localhost:3000/api/auth/callback/google,https://abaci.one/api/auth/callback/google"
|
||||
|
||||
echo -e "${YELLOW}Project ID:${NC} $PROJECT_ID"
|
||||
echo ""
|
||||
|
||||
# Step 1: Check if gcloud is installed
|
||||
echo -e "${BLUE}[1/9] Checking gcloud installation...${NC}"
|
||||
if ! command -v gcloud &> /dev/null; then
|
||||
echo -e "${RED}Error: gcloud CLI not found${NC}"
|
||||
echo "Install it with: brew install google-cloud-sdk"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ gcloud CLI found${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 2: Authenticate with Google
|
||||
echo -e "${BLUE}[2/9] Authenticating with Google...${NC}"
|
||||
CURRENT_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null || echo "")
|
||||
if [ -z "$CURRENT_ACCOUNT" ]; then
|
||||
echo "No active account found. Opening browser to authenticate..."
|
||||
gcloud auth login
|
||||
CURRENT_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)")
|
||||
fi
|
||||
echo -e "${GREEN}✓ Authenticated as: $CURRENT_ACCOUNT${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 3: Create GCP project
|
||||
echo -e "${BLUE}[3/9] Creating GCP project...${NC}"
|
||||
echo "Creating project: $PROJECT_ID"
|
||||
gcloud projects create "$PROJECT_ID" --name="$PROJECT_NAME" 2>/dev/null || {
|
||||
echo -e "${YELLOW}Project might already exist, continuing...${NC}"
|
||||
}
|
||||
gcloud config set project "$PROJECT_ID"
|
||||
echo -e "${GREEN}✓ Project created/selected: $PROJECT_ID${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 4: Check billing (required for APIs)
|
||||
echo -e "${BLUE}[4/9] Checking billing account...${NC}"
|
||||
echo -e "${YELLOW}Note: Google Classroom API requires a billing account, but it's FREE for educational use.${NC}"
|
||||
echo -e "${YELLOW}You won't be charged unless you explicitly enable paid services.${NC}"
|
||||
echo ""
|
||||
|
||||
# List available billing accounts
|
||||
BILLING_ACCOUNTS=$(gcloud billing accounts list --format="value(name)" 2>/dev/null || echo "")
|
||||
if [ -z "$BILLING_ACCOUNTS" ]; then
|
||||
echo -e "${YELLOW}No billing accounts found.${NC}"
|
||||
echo -e "${YELLOW}You'll need to create one at: https://console.cloud.google.com/billing${NC}"
|
||||
echo -e "${YELLOW}Press Enter after creating a billing account, or Ctrl+C to exit${NC}"
|
||||
read -r
|
||||
BILLING_ACCOUNTS=$(gcloud billing accounts list --format="value(name)")
|
||||
fi
|
||||
|
||||
# If multiple accounts, let user choose
|
||||
BILLING_COUNT=$(echo "$BILLING_ACCOUNTS" | wc -l | tr -d ' ')
|
||||
if [ "$BILLING_COUNT" -eq 1 ]; then
|
||||
BILLING_ACCOUNT="$BILLING_ACCOUNTS"
|
||||
else
|
||||
echo "Available billing accounts:"
|
||||
gcloud billing accounts list
|
||||
echo ""
|
||||
echo -n "Enter billing account ID (e.g., 012345-ABCDEF-678901): "
|
||||
read -r BILLING_ACCOUNT
|
||||
fi
|
||||
|
||||
# Link billing account to project
|
||||
echo "Linking billing account to project..."
|
||||
gcloud billing projects link "$PROJECT_ID" --billing-account="$BILLING_ACCOUNT"
|
||||
echo -e "${GREEN}✓ Billing account linked${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 5: Enable required APIs
|
||||
echo -e "${BLUE}[5/9] Enabling required APIs...${NC}"
|
||||
echo "This may take 1-2 minutes..."
|
||||
gcloud services enable classroom.googleapis.com --project="$PROJECT_ID"
|
||||
gcloud services enable people.googleapis.com --project="$PROJECT_ID"
|
||||
echo -e "${GREEN}✓ APIs enabled:${NC}"
|
||||
echo " - Google Classroom API"
|
||||
echo " - Google People API (for profile info)"
|
||||
echo ""
|
||||
|
||||
# Step 6: Create OAuth 2.0 credentials
|
||||
echo -e "${BLUE}[6/9] Creating OAuth 2.0 credentials...${NC}"
|
||||
|
||||
# First check if credentials already exist
|
||||
EXISTING_CREDS=$(gcloud auth application-default print-access-token &>/dev/null && \
|
||||
curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
|
||||
"https://oauth2.googleapis.com/v1/projects/${PROJECT_ID}/oauthClients" 2>/dev/null | \
|
||||
grep -c "clientId" || echo "0")
|
||||
|
||||
if [ "$EXISTING_CREDS" -gt 0 ]; then
|
||||
echo -e "${YELLOW}OAuth credentials already exist for this project${NC}"
|
||||
echo "Skipping credential creation..."
|
||||
else
|
||||
# Create OAuth client
|
||||
# Note: This creates a "Web application" type OAuth client
|
||||
echo "Creating OAuth 2.0 client..."
|
||||
|
||||
# Unfortunately, gcloud doesn't have a direct command for this
|
||||
# We need to use the REST API
|
||||
echo -e "${YELLOW}Note: OAuth client creation requires using the web console${NC}"
|
||||
echo -e "${YELLOW}Opening the OAuth credentials page...${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Please follow these steps in the browser:${NC}"
|
||||
echo "1. Click 'Create Credentials' → 'OAuth client ID'"
|
||||
echo "2. Application type: 'Web application'"
|
||||
echo "3. Name: 'Soroban Abacus Web'"
|
||||
echo "4. Authorized JavaScript origins:"
|
||||
echo " - http://localhost:3000"
|
||||
echo " - https://abaci.one"
|
||||
echo "5. Authorized redirect URIs:"
|
||||
echo " - http://localhost:3000/api/auth/callback/google"
|
||||
echo " - https://abaci.one/api/auth/callback/google"
|
||||
echo "6. Click 'Create'"
|
||||
echo "7. Copy the Client ID and Client Secret"
|
||||
echo ""
|
||||
|
||||
# Open browser to credentials page
|
||||
open "https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID" 2>/dev/null || \
|
||||
echo "Open this URL: https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID"
|
||||
|
||||
echo -n "Press Enter after creating the OAuth client..."
|
||||
read -r
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ OAuth credentials configured${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 7: Configure OAuth Consent Screen
|
||||
echo -e "${BLUE}[7/9] Configuring OAuth consent screen...${NC}"
|
||||
echo -e "${YELLOW}The OAuth consent screen requires web console configuration${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Please follow these steps:${NC}"
|
||||
echo "1. User Type: 'External' (unless you have Google Workspace)"
|
||||
echo "2. App name: 'Soroban Abacus Flashcards'"
|
||||
echo "3. User support email: Your email"
|
||||
echo "4. Developer contact: Your email"
|
||||
echo "5. Scopes: Click 'Add or Remove Scopes' and add:"
|
||||
echo " - .../auth/userinfo.email"
|
||||
echo " - .../auth/userinfo.profile"
|
||||
echo " - .../auth/classroom.courses.readonly"
|
||||
echo " - .../auth/classroom.rosters.readonly"
|
||||
echo "6. Test users: Add your email for testing"
|
||||
echo "7. Save and continue"
|
||||
echo ""
|
||||
|
||||
# Open OAuth consent screen configuration
|
||||
open "https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID" 2>/dev/null || \
|
||||
echo "Open this URL: https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID"
|
||||
|
||||
echo -n "Press Enter after configuring the consent screen..."
|
||||
read -r
|
||||
|
||||
echo -e "${GREEN}✓ OAuth consent screen configured${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 8: Create .env.local file
|
||||
echo -e "${BLUE}[8/9] Creating environment configuration...${NC}"
|
||||
echo ""
|
||||
echo "Please enter your OAuth credentials from the previous step:"
|
||||
echo -n "Client ID: "
|
||||
read -r CLIENT_ID
|
||||
echo -n "Client Secret: "
|
||||
read -r -s CLIENT_SECRET
|
||||
echo ""
|
||||
|
||||
# Create or update .env.local
|
||||
ENV_FILE=".env.local"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Backing up existing $ENV_FILE to ${ENV_FILE}.backup"
|
||||
cp "$ENV_FILE" "${ENV_FILE}.backup"
|
||||
fi
|
||||
|
||||
# Add Google OAuth credentials
|
||||
echo "" >> "$ENV_FILE"
|
||||
echo "# Google OAuth (Generated by setup-google-classroom.sh)" >> "$ENV_FILE"
|
||||
echo "GOOGLE_CLIENT_ID=\"$CLIENT_ID\"" >> "$ENV_FILE"
|
||||
echo "GOOGLE_CLIENT_SECRET=\"$CLIENT_SECRET\"" >> "$ENV_FILE"
|
||||
echo "" >> "$ENV_FILE"
|
||||
|
||||
echo -e "${GREEN}✓ Environment variables added to $ENV_FILE${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 9: Summary
|
||||
echo -e "${BLUE}[9/9] Setup Complete!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}Setup Summary${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "Project ID: ${BLUE}$PROJECT_ID${NC}"
|
||||
echo -e "Project Name: ${BLUE}$PROJECT_NAME${NC}"
|
||||
echo -e "Billing Account: ${BLUE}$BILLING_ACCOUNT${NC}"
|
||||
echo -e "APIs Enabled: ${BLUE}Classroom, People${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next Steps:${NC}"
|
||||
echo "1. Add Google provider to NextAuth configuration"
|
||||
echo "2. Test login with 'Sign in with Google'"
|
||||
echo "3. Verify Classroom API access"
|
||||
echo ""
|
||||
echo -e "${BLUE}Useful Commands:${NC}"
|
||||
echo " View project: gcloud projects describe $PROJECT_ID"
|
||||
echo " List APIs: gcloud services list --enabled --project=$PROJECT_ID"
|
||||
echo " View quota: gcloud quotas describe classroom.googleapis.com --project=$PROJECT_ID"
|
||||
echo ""
|
||||
echo -e "${GREEN}Setup script complete!${NC}"
|
||||
@@ -1,109 +0,0 @@
|
||||
import {
|
||||
DIFFICULTY_PROFILES,
|
||||
makeHarder,
|
||||
makeEasier,
|
||||
findRegroupingIndex,
|
||||
findScaffoldingIndex,
|
||||
REGROUPING_PROGRESSION,
|
||||
SCAFFOLDING_PROGRESSION,
|
||||
} from "../src/app/create/worksheets/addition/difficultyProfiles";
|
||||
|
||||
// Start from beginner
|
||||
let state = {
|
||||
pAnyStart: DIFFICULTY_PROFILES.beginner.regrouping.pAnyStart,
|
||||
pAllStart: DIFFICULTY_PROFILES.beginner.regrouping.pAllStart,
|
||||
displayRules: DIFFICULTY_PROFILES.beginner.displayRules,
|
||||
};
|
||||
|
||||
console.log("=== MAKE HARDER PATH ===\n");
|
||||
console.log("Format: (regroupingIdx, scaffoldingIdx) - description\n");
|
||||
|
||||
const harderPath: Array<{ r: number; s: number; desc: string }> = [];
|
||||
|
||||
// Record starting point
|
||||
let rIdx = findRegroupingIndex(state.pAnyStart, state.pAllStart);
|
||||
let sIdx = findScaffoldingIndex(state.displayRules);
|
||||
harderPath.push({ r: rIdx, s: sIdx, desc: "START (beginner)" });
|
||||
console.log(`(${rIdx}, ${sIdx}) - START (beginner)`);
|
||||
|
||||
// Click "Make Harder" 30 times or until max
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const result = makeHarder(state);
|
||||
|
||||
const newR = findRegroupingIndex(result.pAnyStart, result.pAllStart);
|
||||
const newS = findScaffoldingIndex(result.displayRules);
|
||||
|
||||
if (newR === rIdx && newS === sIdx) {
|
||||
console.log(`\n(${newR}, ${newS}) - ${result.changeDescription} (STOPPED)`);
|
||||
break;
|
||||
}
|
||||
|
||||
rIdx = newR;
|
||||
sIdx = newS;
|
||||
state = result;
|
||||
|
||||
harderPath.push({ r: rIdx, s: sIdx, desc: result.changeDescription });
|
||||
console.log(`(${rIdx}, ${sIdx}) - ${result.changeDescription}`);
|
||||
}
|
||||
|
||||
console.log("\n\n=== PATH VISUALIZATION ===\n");
|
||||
console.log("Regrouping Index →");
|
||||
console.log("Scaffolding ↓\n");
|
||||
|
||||
// Create 2D grid visualization
|
||||
const grid: string[][] = [];
|
||||
for (let s = 0; s <= 12; s++) {
|
||||
grid[s] = [];
|
||||
for (let r = 0; r <= 18; r++) {
|
||||
grid[s][r] = " ·";
|
||||
}
|
||||
}
|
||||
|
||||
// Mark path
|
||||
harderPath.forEach((point, idx) => {
|
||||
if (idx === 0) {
|
||||
grid[point.s][point.r] = " S"; // Start
|
||||
} else if (idx === harderPath.length - 1) {
|
||||
grid[point.s][point.r] = " E"; // End
|
||||
} else {
|
||||
grid[point.s][point.r] = `${idx.toString().padStart(3)}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Mark presets
|
||||
const presets = [
|
||||
{ label: "BEG", profile: DIFFICULTY_PROFILES.beginner },
|
||||
{ label: "EAR", profile: DIFFICULTY_PROFILES.earlyLearner },
|
||||
{ label: "INT", profile: DIFFICULTY_PROFILES.intermediate },
|
||||
{ label: "ADV", profile: DIFFICULTY_PROFILES.advanced },
|
||||
{ label: "EXP", profile: DIFFICULTY_PROFILES.expert },
|
||||
];
|
||||
|
||||
presets.forEach((preset) => {
|
||||
const r = findRegroupingIndex(
|
||||
preset.profile.regrouping.pAnyStart,
|
||||
preset.profile.regrouping.pAllStart,
|
||||
);
|
||||
const s = findScaffoldingIndex(preset.profile.displayRules);
|
||||
|
||||
// Only mark if not already part of path
|
||||
const onPath = harderPath.some((p) => p.r === r && p.s === s);
|
||||
if (!onPath) {
|
||||
grid[s][r] = preset.label;
|
||||
}
|
||||
});
|
||||
|
||||
// Print grid (inverted so scaffolding increases upward)
|
||||
console.log(
|
||||
" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18",
|
||||
);
|
||||
for (let s = 12; s >= 0; s--) {
|
||||
console.log(`${s.toString().padStart(2)} ${grid[s].join("")}`);
|
||||
}
|
||||
|
||||
console.log("\nLegend:");
|
||||
console.log(" S = Start (beginner)");
|
||||
console.log(" E = End (maximum)");
|
||||
console.log(" 1-29 = Step number");
|
||||
console.log(" BEG/EAR/INT/ADV/EXP = Preset profiles");
|
||||
console.log(" · = Not visited");
|
||||
@@ -34,36 +34,10 @@ app.prepare().then(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Debug: Check upgrade handlers at each stage
|
||||
console.log('📊 Stage 1 - After server creation:')
|
||||
console.log(` Upgrade handlers: ${server.listeners('upgrade').length}`)
|
||||
|
||||
// Initialize Socket.IO
|
||||
const { initializeSocketServer } = require('./dist/socket-server')
|
||||
|
||||
console.log('📊 Stage 2 - Before initializeSocketServer:')
|
||||
console.log(` Upgrade handlers: ${server.listeners('upgrade').length}`)
|
||||
|
||||
initializeSocketServer(server)
|
||||
|
||||
console.log('📊 Stage 3 - After initializeSocketServer:')
|
||||
const allHandlers = server.listeners('upgrade')
|
||||
console.log(` Upgrade handlers: ${allHandlers.length}`)
|
||||
allHandlers.forEach((handler, i) => {
|
||||
console.log(` [${i}] ${handler.name || 'anonymous'} (length: ${handler.length} params)`)
|
||||
})
|
||||
|
||||
// Log all upgrade requests to see handler execution order
|
||||
const originalEmit = server.emit.bind(server)
|
||||
server.emit = (event, ...args) => {
|
||||
if (event === 'upgrade') {
|
||||
const req = args[0]
|
||||
console.log(`\n🔄 UPGRADE REQUEST: ${req.url}`)
|
||||
console.log(` ${allHandlers.length} handlers will be called`)
|
||||
}
|
||||
return originalEmit(event, ...args)
|
||||
}
|
||||
|
||||
server
|
||||
.once('error', (err) => {
|
||||
console.error(err)
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getPlayer, getRoomActivePlayers, setPlayerActiveStatus } from '@/lib/arcade/player-manager'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/arcade/rooms/:roomId/deactivate-player
|
||||
* Deactivate a specific player in the room (host only)
|
||||
* Body:
|
||||
* - playerId: string - The player to deactivate
|
||||
*/
|
||||
export async function POST(req: NextRequest, context: RouteContext) {
|
||||
console.log('[Deactivate Player API] POST request received')
|
||||
|
||||
try {
|
||||
const { roomId } = await context.params
|
||||
console.log('[Deactivate Player API] roomId:', roomId)
|
||||
|
||||
const viewerId = await getViewerId()
|
||||
console.log('[Deactivate Player API] viewerId:', viewerId)
|
||||
|
||||
const body = await req.json()
|
||||
console.log('[Deactivate Player API] body:', body)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.playerId) {
|
||||
console.log('[Deactivate Player API] Missing playerId in body')
|
||||
return NextResponse.json({ error: 'Missing required field: playerId' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if user is the host
|
||||
console.log('[Deactivate Player API] Fetching room members for roomId:', roomId)
|
||||
const members = await getRoomMembers(roomId)
|
||||
console.log('[Deactivate Player API] members count:', members.length)
|
||||
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
console.log('[Deactivate Player API] currentMember:', currentMember)
|
||||
|
||||
if (!currentMember) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can deactivate players' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Get the player
|
||||
console.log('[Deactivate Player API] Looking up player with ID:', body.playerId)
|
||||
const player = await getPlayer(body.playerId)
|
||||
console.log('[Deactivate Player API] Player found:', player)
|
||||
|
||||
if (!player) {
|
||||
console.log('[Deactivate Player API] Player not found in database')
|
||||
return NextResponse.json({ error: 'Player not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
console.log('[Deactivate Player API] Player userId:', player.userId)
|
||||
console.log(
|
||||
'[Deactivate Player API] Room member userIds:',
|
||||
members.map((m) => m.userId)
|
||||
)
|
||||
|
||||
// Can't deactivate your own players (use the regular player controls for that)
|
||||
if (player.userId === viewerId) {
|
||||
console.log('[Deactivate Player API] ERROR: Cannot deactivate your own players')
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot deactivate your own players. Use the player controls in the nav.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Note: We don't check if the player belongs to a current room member
|
||||
// because players from users who have left the room may still need to be cleaned up
|
||||
console.log('[Deactivate Player API] Player validation passed, proceeding with deactivation')
|
||||
|
||||
// Deactivate the player
|
||||
await setPlayerActiveStatus(body.playerId, false)
|
||||
|
||||
// Broadcast updates via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
// Get updated player list
|
||||
const memberPlayers = await getRoomActivePlayers(roomId)
|
||||
|
||||
// Convert memberPlayers Map to object for JSON serialization
|
||||
const memberPlayersObj: Record<string, any[]> = {}
|
||||
for (const [uid, players] of memberPlayers.entries()) {
|
||||
memberPlayersObj[uid] = players
|
||||
}
|
||||
|
||||
// Notify everyone in the room about the player update
|
||||
io.to(`room:${roomId}`).emit('player-deactivated', {
|
||||
roomId,
|
||||
playerId: body.playerId,
|
||||
playerName: player.name,
|
||||
deactivatedBy: currentMember.displayName,
|
||||
memberPlayers: memberPlayersObj,
|
||||
})
|
||||
|
||||
console.log(
|
||||
`[Deactivate Player API] Player ${body.playerId} (${player.name}) deactivated by host in room ${roomId}`
|
||||
)
|
||||
} catch (socketError) {
|
||||
console.error('[Deactivate Player API] Failed to broadcast deactivation:', socketError)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Deactivate Player API] Success - returning 200')
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
console.error('[Deactivate Player API] ERROR:', error)
|
||||
console.error('[Deactivate Player API] Error stack:', error.stack)
|
||||
return NextResponse.json({ error: 'Failed to deactivate player' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getActivePlayers, getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { getInvitation, acceptInvitation } from '@/lib/arcade/room-invitations'
|
||||
import { getInvitation } from '@/lib/arcade/room-invitations'
|
||||
import { getJoinRequest } from '@/lib/arcade/room-join-requests'
|
||||
import { getRoomById, touchRoom } from '@/lib/arcade/room-manager'
|
||||
import { addRoomMember, getRoomMembers } from '@/lib/arcade/room-membership'
|
||||
@@ -26,19 +26,12 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json().catch(() => ({}))
|
||||
|
||||
console.log(`[Join API] User ${viewerId} attempting to join room ${roomId}`)
|
||||
|
||||
// Get room
|
||||
const room = await getRoomById(roomId)
|
||||
if (!room) {
|
||||
console.log(`[Join API] Room ${roomId} not found`)
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Join API] Room ${roomId} found: name="${room.name}" accessMode="${room.accessMode}" game="${room.gameName}"`
|
||||
)
|
||||
|
||||
// Check if user is banned
|
||||
const banned = await isUserBanned(roomId, viewerId)
|
||||
if (banned) {
|
||||
@@ -50,20 +43,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
const isExistingMember = members.some((m) => m.userId === viewerId)
|
||||
const isRoomCreator = room.createdBy === viewerId
|
||||
|
||||
// Track invitation/join request to mark as accepted after successful join
|
||||
let invitationToAccept: string | null = null
|
||||
let joinRequestToAccept: string | null = null
|
||||
|
||||
// Check for pending invitation (regardless of access mode)
|
||||
// This ensures invitations are marked as accepted when user joins ANY room type
|
||||
const invitation = await getInvitation(roomId, viewerId)
|
||||
if (invitation && invitation.status === 'pending') {
|
||||
invitationToAccept = invitation.id
|
||||
console.log(
|
||||
`[Join API] Found pending invitation ${invitation.id} for user ${viewerId} in room ${roomId}`
|
||||
)
|
||||
}
|
||||
|
||||
// Validate access mode
|
||||
switch (room.accessMode) {
|
||||
case 'locked':
|
||||
@@ -104,20 +83,16 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
}
|
||||
|
||||
case 'restricted': {
|
||||
console.log(`[Join API] Room is restricted, checking invitation for user ${viewerId}`)
|
||||
// Room creator can always rejoin their own room
|
||||
if (!isRoomCreator) {
|
||||
// For restricted rooms, invitation is REQUIRED
|
||||
if (!invitationToAccept) {
|
||||
console.log(`[Join API] No valid pending invitation, rejecting join`)
|
||||
// Check for valid pending invitation
|
||||
const invitation = await getInvitation(roomId, viewerId)
|
||||
if (!invitation || invitation.status !== 'pending') {
|
||||
return NextResponse.json(
|
||||
{ error: 'You need a valid invitation to join this room' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
console.log(`[Join API] Valid invitation found, will accept after member added`)
|
||||
} else {
|
||||
console.log(`[Join API] User is room creator, skipping invitation check`)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -133,9 +108,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
// Note: Join request stays in "approved" status after join
|
||||
// (No need to update it - "approved" indicates they were allowed in)
|
||||
joinRequestToAccept = joinRequest.id
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -163,13 +135,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
isCreator: false,
|
||||
})
|
||||
|
||||
// Mark invitation as accepted (if applicable)
|
||||
if (invitationToAccept) {
|
||||
await acceptInvitation(invitationToAccept)
|
||||
console.log(`[Join API] Accepted invitation ${invitationToAccept} for user ${viewerId}`)
|
||||
}
|
||||
// Note: Join requests stay in "approved" status (no need to update)
|
||||
|
||||
// Fetch user's active players (these will participate in the game)
|
||||
const activePlayers = await getActivePlayers(viewerId)
|
||||
|
||||
@@ -205,10 +170,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
}
|
||||
|
||||
// Build response with auto-leave info if applicable
|
||||
console.log(
|
||||
`[Join API] Successfully added user ${viewerId} to room ${roomId} (invitation=${invitationToAccept ? 'accepted' : 'N/A'})`
|
||||
)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
member,
|
||||
|
||||
@@ -17,17 +17,12 @@ type RouteContext = {
|
||||
|
||||
/**
|
||||
* PATCH /api/arcade/rooms/:roomId/settings
|
||||
* Update room settings
|
||||
*
|
||||
* Authorization:
|
||||
* - gameConfig: Any room member can update
|
||||
* - All other settings: Host only
|
||||
*
|
||||
* Update room settings (host only)
|
||||
* Body:
|
||||
* - accessMode?: 'open' | 'locked' | 'retired' | 'password' | 'restricted' | 'approval-only' (host only)
|
||||
* - password?: string (plain text, will be hashed) (host only)
|
||||
* - gameName?: string | null (any game with a registered validator) (host only)
|
||||
* - gameConfig?: object (game-specific settings) (any member)
|
||||
* - accessMode?: 'open' | 'locked' | 'retired' | 'password' | 'restricted' | 'approval-only'
|
||||
* - password?: string (plain text, will be hashed)
|
||||
* - gameName?: string | null (any game with a registered validator)
|
||||
* - gameConfig?: object (game-specific settings)
|
||||
*
|
||||
* Note: gameName is validated at runtime against the validator registry.
|
||||
* No need to update this file when adding new games!
|
||||
@@ -68,7 +63,7 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
)
|
||||
)
|
||||
|
||||
// Check if user is a room member
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
|
||||
@@ -76,24 +71,8 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Determine which settings are being changed
|
||||
const changingRoomSettings = !!(
|
||||
body.accessMode !== undefined ||
|
||||
body.password !== undefined ||
|
||||
body.gameName !== undefined ||
|
||||
body.name !== undefined ||
|
||||
body.description !== undefined
|
||||
)
|
||||
|
||||
// Only gameConfig can be changed by any member
|
||||
// All other settings require host privileges
|
||||
if (changingRoomSettings && !currentMember.isCreator) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Only the host can change room settings (name, access mode, game selection, etc.)',
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
if (!currentMember.isCreator) {
|
||||
return NextResponse.json({ error: 'Only the host can change room settings' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Validate accessMode if provided
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getFeaturedPosts } from '@/lib/blog'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const posts = await getFeaturedPosts()
|
||||
return NextResponse.json(posts)
|
||||
} catch (error) {
|
||||
console.error('Error fetching featured posts:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch featured posts' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import { generateMonthlyTypst, generateDailyTypst, getDaysInMonth } from '../utils/typstGenerator'
|
||||
import type { AbacusConfig } from '@soroban/abacus-react'
|
||||
import { generateCalendarComposite } from '@/utils/calendar/generateCalendarComposite'
|
||||
import { generateAbacusElement } from '@/utils/calendar/generateCalendarAbacus'
|
||||
|
||||
interface CalendarRequest {
|
||||
month: number
|
||||
year: number
|
||||
format: 'monthly' | 'daily'
|
||||
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
|
||||
abacusConfig?: AbacusConfig
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let tempDir: string | null = null
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid Next.js bundler issues with react-dom/server
|
||||
const { renderToStaticMarkup } = await import('react-dom/server')
|
||||
|
||||
const body: CalendarRequest = await request.json()
|
||||
const { month, year, format, paperSize, abacusConfig } = body
|
||||
|
||||
// Validate inputs
|
||||
if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
|
||||
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create temp directory for SVG files
|
||||
tempDir = join(tmpdir(), `calendar-${Date.now()}-${Math.random()}`)
|
||||
mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
// Generate and write SVG files
|
||||
const daysInMonth = getDaysInMonth(year, month)
|
||||
let typstContent: string
|
||||
|
||||
if (format === 'monthly') {
|
||||
// Generate single composite SVG for monthly calendar
|
||||
const calendarSvg = generateCalendarComposite({
|
||||
month,
|
||||
year,
|
||||
renderToString: renderToStaticMarkup,
|
||||
})
|
||||
if (!calendarSvg || calendarSvg.trim().length === 0) {
|
||||
throw new Error('Generated empty composite calendar SVG')
|
||||
}
|
||||
writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
|
||||
|
||||
// Generate Typst document
|
||||
typstContent = generateMonthlyTypst({
|
||||
month,
|
||||
year,
|
||||
paperSize,
|
||||
daysInMonth,
|
||||
})
|
||||
} else {
|
||||
// Daily format: generate individual SVGs for each day
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const svg = renderToStaticMarkup(generateAbacusElement(day, 2))
|
||||
if (!svg || svg.trim().length === 0) {
|
||||
throw new Error(`Generated empty SVG for day ${day}`)
|
||||
}
|
||||
writeFileSync(join(tempDir, `day-${day}.svg`), svg)
|
||||
}
|
||||
|
||||
// Generate year SVG
|
||||
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
|
||||
const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
|
||||
if (!yearSvg || yearSvg.trim().length === 0) {
|
||||
throw new Error(`Generated empty SVG for year ${year}`)
|
||||
}
|
||||
writeFileSync(join(tempDir, 'year.svg'), yearSvg)
|
||||
|
||||
// Generate Typst document
|
||||
typstContent = generateDailyTypst({
|
||||
month,
|
||||
year,
|
||||
paperSize,
|
||||
daysInMonth,
|
||||
})
|
||||
}
|
||||
|
||||
// Compile with Typst: stdin for .typ content, stdout for PDF output
|
||||
let pdfBuffer: Buffer
|
||||
try {
|
||||
pdfBuffer = execSync('typst compile --format pdf - -', {
|
||||
input: typstContent,
|
||||
cwd: tempDir, // Run in temp dir so relative paths work
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB limit for large calendars
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Typst compilation error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to compile PDF. Is Typst installed?' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
tempDir = null
|
||||
|
||||
// Return JSON with PDF
|
||||
return NextResponse.json({
|
||||
pdf: pdfBuffer.toString('base64'),
|
||||
filename: `calendar-${year}-${String(month).padStart(2, '0')}.pdf`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating calendar:', error)
|
||||
|
||||
// Clean up temp directory if it exists
|
||||
if (tempDir) {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to clean up temp directory:', cleanupError)
|
||||
}
|
||||
}
|
||||
|
||||
// Surface the actual error for debugging
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? error.stack : undefined
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to generate calendar',
|
||||
message: errorMessage,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: errorStack }),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import { generateMonthlyTypst, getDaysInMonth } from '../utils/typstGenerator'
|
||||
import { generateCalendarComposite } from '@/utils/calendar/generateCalendarComposite'
|
||||
import { generateAbacusElement } from '@/utils/calendar/generateCalendarAbacus'
|
||||
|
||||
interface PreviewRequest {
|
||||
month: number
|
||||
year: number
|
||||
format: 'monthly' | 'daily'
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let tempDir: string | null = null
|
||||
|
||||
try {
|
||||
const body: PreviewRequest = await request.json()
|
||||
const { month, year, format } = body
|
||||
|
||||
// Validate inputs
|
||||
if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
|
||||
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Dynamic import to avoid Next.js bundler issues
|
||||
const { renderToStaticMarkup } = await import('react-dom/server')
|
||||
|
||||
// Create temp directory for SVG file(s)
|
||||
tempDir = join(tmpdir(), `calendar-preview-${Date.now()}-${Math.random()}`)
|
||||
mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
// Generate Typst document content
|
||||
const daysInMonth = getDaysInMonth(year, month)
|
||||
let typstContent: string
|
||||
|
||||
if (format === 'monthly') {
|
||||
// Generate and write composite SVG
|
||||
const calendarSvg = generateCalendarComposite({
|
||||
month,
|
||||
year,
|
||||
renderToString: renderToStaticMarkup,
|
||||
})
|
||||
writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
|
||||
|
||||
typstContent = generateMonthlyTypst({
|
||||
month,
|
||||
year,
|
||||
paperSize: 'us-letter',
|
||||
daysInMonth,
|
||||
})
|
||||
} else {
|
||||
// Daily format: Create a SINGLE composite SVG (like monthly) to avoid multi-image export issue
|
||||
|
||||
// Generate individual abacus SVGs
|
||||
const daySvg = renderToStaticMarkup(generateAbacusElement(1, 2))
|
||||
if (!daySvg || daySvg.trim().length === 0) {
|
||||
throw new Error('Generated empty SVG for day 1')
|
||||
}
|
||||
|
||||
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
|
||||
const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
|
||||
if (!yearSvg || yearSvg.trim().length === 0) {
|
||||
throw new Error(`Generated empty SVG for year ${year}`)
|
||||
}
|
||||
|
||||
// Create composite SVG with both year and day abacus
|
||||
const monthName = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
][month - 1]
|
||||
const dayOfWeek = new Date(year, month - 1, 1).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
})
|
||||
|
||||
// Extract SVG content (remove outer <svg> tags)
|
||||
const yearSvgContent = yearSvg.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
|
||||
const daySvgContent = daySvg.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
|
||||
|
||||
// Create composite SVG (850x1100 = US Letter aspect ratio)
|
||||
const compositeWidth = 850
|
||||
const compositeHeight = 1100
|
||||
const yearAbacusWidth = 120 // Natural width at scale 1
|
||||
const yearAbacusHeight = 230
|
||||
const dayAbacusWidth = 120
|
||||
const dayAbacusHeight = 230
|
||||
|
||||
const compositeSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${compositeWidth}" height="${compositeHeight}" viewBox="0 0 ${compositeWidth} ${compositeHeight}">
|
||||
<!-- Background -->
|
||||
<rect width="${compositeWidth}" height="${compositeHeight}" fill="white"/>
|
||||
|
||||
<!-- Decorative border -->
|
||||
<rect x="40" y="40" width="${compositeWidth - 80}" height="${compositeHeight - 80}" fill="none" stroke="#2563eb" stroke-width="3" rx="8"/>
|
||||
<rect x="50" y="50" width="${compositeWidth - 100}" height="${compositeHeight - 100}" fill="none" stroke="#2563eb" stroke-width="1" rx="4"/>
|
||||
|
||||
<!-- Header section with background -->
|
||||
<rect x="70" y="70" width="${compositeWidth - 140}" height="120" fill="#eff6ff" stroke="#2563eb" stroke-width="2" rx="6"/>
|
||||
|
||||
<!-- Month name -->
|
||||
<text x="${compositeWidth / 2}" y="125" text-anchor="middle" font-family="Georgia, serif" font-size="48" font-weight="bold" fill="#1e40af" letter-spacing="2">
|
||||
${monthName.toUpperCase()}
|
||||
</text>
|
||||
|
||||
<!-- Year abacus (smaller, in header) -->
|
||||
<svg x="${compositeWidth / 2 - yearAbacusWidth * 0.4}" y="140" width="${yearAbacusWidth * 0.8}" height="${yearAbacusHeight * 0.8}" viewBox="0 0 ${yearAbacusWidth} ${yearAbacusHeight}">
|
||||
${yearSvgContent}
|
||||
</svg>
|
||||
|
||||
<!-- Day of week (large and prominent) -->
|
||||
<text x="${compositeWidth / 2}" y="260" text-anchor="middle" font-family="Georgia, serif" font-size="42" font-weight="bold" fill="#1e3a8a">
|
||||
${dayOfWeek}
|
||||
</text>
|
||||
|
||||
<!-- Day abacus (much larger, main focus) -->
|
||||
<svg x="${compositeWidth / 2 - dayAbacusWidth * 1.25}" y="300" width="${dayAbacusWidth * 2.5}" height="${dayAbacusHeight * 2.5}" viewBox="0 0 ${dayAbacusWidth} ${dayAbacusHeight}">
|
||||
${daySvgContent}
|
||||
</svg>
|
||||
|
||||
<!-- Full date (below day abacus) -->
|
||||
<text x="${compositeWidth / 2}" y="890" text-anchor="middle" font-family="Georgia, serif" font-size="24" font-weight="500" fill="#475569">
|
||||
${monthName} 1, ${year}
|
||||
</text>
|
||||
|
||||
<!-- Notes section with decorative box -->
|
||||
<rect x="70" y="930" width="${compositeWidth - 140}" height="120" fill="#fefce8" stroke="#ca8a04" stroke-width="2" rx="4"/>
|
||||
<text x="90" y="960" font-family="Georgia, serif" font-size="18" font-weight="bold" fill="#854d0e">
|
||||
Notes:
|
||||
</text>
|
||||
<line x1="90" y1="980" x2="${compositeWidth - 90}" y2="980" stroke="#ca8a04" stroke-width="1"/>
|
||||
<line x1="90" y1="1005" x2="${compositeWidth - 90}" y2="1005" stroke="#ca8a04" stroke-width="1"/>
|
||||
<line x1="90" y1="1030" x2="${compositeWidth - 90}" y2="1030" stroke="#ca8a04" stroke-width="1"/>
|
||||
</svg>`
|
||||
|
||||
writeFileSync(join(tempDir, 'daily-preview.svg'), compositeSvg)
|
||||
|
||||
// Use single composite image (like monthly)
|
||||
typstContent = `#set page(
|
||||
paper: "us-letter",
|
||||
margin: (x: 0.5in, y: 0.5in),
|
||||
)
|
||||
|
||||
#align(center + horizon)[
|
||||
#image("daily-preview.svg", width: 100%, fit: "contain")
|
||||
]
|
||||
`
|
||||
}
|
||||
|
||||
// Compile with Typst: stdin for .typ content, stdout for SVG output
|
||||
let svg: string
|
||||
try {
|
||||
svg = execSync('typst compile --format svg - -', {
|
||||
input: typstContent,
|
||||
encoding: 'utf8',
|
||||
cwd: tempDir, // Run in temp dir so relative paths work
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Typst compilation error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to compile preview. Is Typst installed?' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
tempDir = null
|
||||
|
||||
return NextResponse.json({ svg })
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error)
|
||||
|
||||
// Clean up temp directory if it exists
|
||||
if (tempDir) {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to clean up temp directory:', cleanupError)
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate preview', message: errorMessage },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
interface TypstMonthlyConfig {
|
||||
month: number
|
||||
year: number
|
||||
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
|
||||
daysInMonth: number
|
||||
}
|
||||
|
||||
interface TypstDailyConfig {
|
||||
month: number
|
||||
year: number
|
||||
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
|
||||
daysInMonth: number
|
||||
}
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
]
|
||||
|
||||
export function getDaysInMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 0).getDate()
|
||||
}
|
||||
|
||||
function getFirstDayOfWeek(year: number, month: number): number {
|
||||
return new Date(year, month - 1, 1).getDay() // 0 = Sunday
|
||||
}
|
||||
|
||||
function getDayOfWeek(year: number, month: number, day: number): string {
|
||||
const date = new Date(year, month - 1, day)
|
||||
return date.toLocaleDateString('en-US', { weekday: 'long' })
|
||||
}
|
||||
|
||||
type PaperSize = 'us-letter' | 'a4' | 'a3' | 'tabloid'
|
||||
|
||||
interface PaperConfig {
|
||||
typstName: string
|
||||
marginX: string
|
||||
marginY: string
|
||||
}
|
||||
|
||||
function getPaperConfig(size: string): PaperConfig {
|
||||
const configs: Record<PaperSize, PaperConfig> = {
|
||||
// Tight margins to maximize space for calendar grid
|
||||
'us-letter': { typstName: 'us-letter', marginX: '0.5in', marginY: '0.5in' },
|
||||
// A4 is slightly taller/narrower than US Letter - adjust margins proportionally
|
||||
a4: { typstName: 'a4', marginX: '1.3cm', marginY: '1.3cm' },
|
||||
// A3 is 2x area of A4 - can use same margins but will scale content larger
|
||||
a3: { typstName: 'a3', marginX: '1.5cm', marginY: '1.5cm' },
|
||||
// Tabloid (11" × 17") is larger - can use more margin
|
||||
tabloid: { typstName: 'us-tabloid', marginX: '0.75in', marginY: '0.75in' },
|
||||
}
|
||||
return configs[size as PaperSize] || configs['us-letter']
|
||||
}
|
||||
|
||||
export function generateMonthlyTypst(config: TypstMonthlyConfig): string {
|
||||
const { paperSize } = config
|
||||
const paperConfig = getPaperConfig(paperSize)
|
||||
|
||||
// Single-page design: use one composite SVG that scales to fit
|
||||
// This prevents overflow - Typst will scale the image to fit available space
|
||||
return `#set page(
|
||||
paper: "${paperConfig.typstName}",
|
||||
margin: (x: ${paperConfig.marginX}, y: ${paperConfig.marginY}),
|
||||
)
|
||||
|
||||
// Composite calendar SVG - scales to fit page (prevents multi-page overflow)
|
||||
#align(center + horizon)[
|
||||
#image("calendar.svg", width: 100%, fit: "contain")
|
||||
]
|
||||
`
|
||||
}
|
||||
|
||||
export function generateDailyTypst(config: TypstDailyConfig): string {
|
||||
const { month, year, paperSize, daysInMonth } = config
|
||||
const paperConfig = getPaperConfig(paperSize)
|
||||
const monthName = MONTH_NAMES[month - 1]
|
||||
|
||||
let pages = ''
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dayOfWeek = getDayOfWeek(year, month, day)
|
||||
|
||||
pages += `
|
||||
#page(
|
||||
paper: "${paperConfig.typstName}",
|
||||
margin: (x: ${paperConfig.marginX}, y: ${paperConfig.marginY}),
|
||||
)[
|
||||
#set text(font: "Georgia")
|
||||
|
||||
// Decorative borders
|
||||
#rect(
|
||||
width: 100%,
|
||||
height: 100%,
|
||||
stroke: (paint: rgb("#2563eb"), thickness: 3pt),
|
||||
radius: 8pt,
|
||||
inset: 0pt,
|
||||
)[
|
||||
#rect(
|
||||
width: 100%,
|
||||
height: 100%,
|
||||
stroke: (paint: rgb("#2563eb"), thickness: 1pt),
|
||||
radius: 4pt,
|
||||
inset: 10pt,
|
||||
)[
|
||||
#v(10pt)
|
||||
|
||||
// Header section with background
|
||||
#rect(
|
||||
width: 100%,
|
||||
height: 90pt,
|
||||
fill: rgb("#eff6ff"),
|
||||
stroke: (paint: rgb("#2563eb"), thickness: 2pt),
|
||||
radius: 6pt,
|
||||
)[
|
||||
#align(center)[
|
||||
#v(15pt)
|
||||
#text(size: 32pt, weight: "bold", fill: rgb("#1e40af"), tracking: 2pt)[
|
||||
${monthName.toUpperCase()}
|
||||
]
|
||||
#v(5pt)
|
||||
#image("year.svg", width: 15%)
|
||||
]
|
||||
]
|
||||
|
||||
#v(15pt)
|
||||
|
||||
// Day of week (large and prominent)
|
||||
#align(center)[
|
||||
#text(size: 28pt, weight: "bold", fill: rgb("#1e3a8a"))[
|
||||
${dayOfWeek}
|
||||
]
|
||||
]
|
||||
|
||||
#v(10pt)
|
||||
|
||||
// Day abacus (main focus, large)
|
||||
#align(center)[
|
||||
#image("day-${day}.svg", width: 45%)
|
||||
]
|
||||
|
||||
#v(10pt)
|
||||
|
||||
// Full date
|
||||
#align(center)[
|
||||
#text(size: 18pt, weight: 500, fill: rgb("#475569"))[
|
||||
${monthName} ${day}, ${year}
|
||||
]
|
||||
]
|
||||
|
||||
#v(1fr)
|
||||
|
||||
// Notes section with decorative box
|
||||
#rect(
|
||||
width: 100%,
|
||||
height: 90pt,
|
||||
fill: rgb("#fefce8"),
|
||||
stroke: (paint: rgb("#ca8a04"), thickness: 2pt),
|
||||
radius: 4pt,
|
||||
)[
|
||||
#v(8pt)
|
||||
#text(size: 14pt, weight: "bold", fill: rgb("#854d0e"))[
|
||||
#h(10pt) Notes:
|
||||
]
|
||||
#v(8pt)
|
||||
#line(length: 95%, stroke: (paint: rgb("#ca8a04"), thickness: 1pt))
|
||||
#v(8pt)
|
||||
#line(length: 95%, stroke: (paint: rgb("#ca8a04"), thickness: 1pt))
|
||||
#v(8pt)
|
||||
#line(length: 95%, stroke: (paint: rgb("#ca8a04"), thickness: 1pt))
|
||||
]
|
||||
|
||||
#v(10pt)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
${day < daysInMonth ? '' : ''}`
|
||||
|
||||
if (day < daysInMonth) {
|
||||
pages += '\n'
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import type { FlashcardFormState } from '@/app/create/flashcards/page'
|
||||
import {
|
||||
generateFlashcardFront,
|
||||
generateFlashcardBack,
|
||||
} from '@/utils/flashcards/generateFlashcardSvgs'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Parse range string to get numbers for preview (first page only)
|
||||
*/
|
||||
function parseRangeForPreview(range: string, step: number, cardsPerPage: number): number[] {
|
||||
const numbers: number[] = []
|
||||
|
||||
if (range.includes('-')) {
|
||||
const [start, end] = range.split('-').map((n) => parseInt(n, 10))
|
||||
for (let i = start; i <= end && numbers.length < cardsPerPage; i += step) {
|
||||
numbers.push(i)
|
||||
}
|
||||
} else if (range.includes(',')) {
|
||||
const parts = range.split(',').map((n) => parseInt(n.trim(), 10))
|
||||
numbers.push(...parts.slice(0, cardsPerPage))
|
||||
} else {
|
||||
numbers.push(parseInt(range, 10))
|
||||
}
|
||||
|
||||
return numbers.slice(0, cardsPerPage)
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let tempDir: string | null = null
|
||||
|
||||
try {
|
||||
const body: FlashcardFormState = await request.json()
|
||||
const {
|
||||
range = '0-99',
|
||||
step = 1,
|
||||
cardsPerPage = 6,
|
||||
paperSize = 'us-letter',
|
||||
orientation = 'portrait',
|
||||
beadShape = 'diamond',
|
||||
colorScheme = 'place-value',
|
||||
colorPalette = 'default',
|
||||
hideInactiveBeads = false,
|
||||
showEmptyColumns = false,
|
||||
columns = 'auto',
|
||||
scaleFactor = 0.9,
|
||||
coloredNumerals = false,
|
||||
} = body
|
||||
|
||||
// Dynamic import to avoid Next.js bundler issues
|
||||
const { renderToStaticMarkup } = await import('react-dom/server')
|
||||
|
||||
// Create temp directory for SVG files
|
||||
tempDir = join(tmpdir(), `flashcards-preview-${Date.now()}-${Math.random()}`)
|
||||
mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
// Get numbers for first page only
|
||||
const numbers = parseRangeForPreview(range, step, cardsPerPage)
|
||||
|
||||
if (numbers.length === 0) {
|
||||
return NextResponse.json({ error: 'No valid numbers in range' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Generate SVG files for each card (front and back)
|
||||
const config = {
|
||||
beadShape,
|
||||
colorScheme,
|
||||
colorPalette,
|
||||
hideInactiveBeads,
|
||||
showEmptyColumns,
|
||||
columns: columns === 'auto' ? 'auto' : Number(columns),
|
||||
scaleFactor,
|
||||
coloredNumerals,
|
||||
}
|
||||
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
const num = numbers[i]
|
||||
|
||||
// Generate front (abacus)
|
||||
const frontElement = generateFlashcardFront(num, config)
|
||||
const frontSvg = renderToStaticMarkup(frontElement)
|
||||
writeFileSync(join(tempDir, `card_${i}_front.svg`), frontSvg)
|
||||
|
||||
// Generate back (numeral)
|
||||
const backElement = generateFlashcardBack(num, config)
|
||||
const backSvg = renderToStaticMarkup(backElement)
|
||||
writeFileSync(join(tempDir, `card_${i}_back.svg`), backSvg)
|
||||
}
|
||||
|
||||
// Calculate card dimensions based on paper size and orientation
|
||||
const paperDimensions = {
|
||||
'us-letter': { width: 8.5, height: 11 },
|
||||
a4: { width: 8.27, height: 11.69 },
|
||||
a3: { width: 11.69, height: 16.54 },
|
||||
a5: { width: 5.83, height: 8.27 },
|
||||
}
|
||||
|
||||
const paper = paperDimensions[paperSize] || paperDimensions['us-letter']
|
||||
const [pageWidth, pageHeight] =
|
||||
orientation === 'landscape' ? [paper.height, paper.width] : [paper.width, paper.height]
|
||||
|
||||
// Calculate grid layout (2 columns × 3 rows for 6 cards per page typically)
|
||||
const cols = 2
|
||||
const rows = Math.ceil(cardsPerPage / cols)
|
||||
const margin = 0.5 // inches
|
||||
const gutter = 0.2 // inches between cards
|
||||
|
||||
const availableWidth = pageWidth - 2 * margin - gutter * (cols - 1)
|
||||
const availableHeight = pageHeight - 2 * margin - gutter * (rows - 1)
|
||||
const cardWidth = availableWidth / cols
|
||||
const cardHeight = availableHeight / rows
|
||||
|
||||
// Generate Typst document with card grid
|
||||
const typstContent = `
|
||||
#set page(
|
||||
paper: "${paperSize}",
|
||||
margin: (x: ${margin}in, y: ${margin}in),
|
||||
flipped: ${orientation === 'landscape'},
|
||||
)
|
||||
|
||||
// Grid layout for flashcards preview (first page only)
|
||||
#grid(
|
||||
columns: ${cols},
|
||||
rows: ${rows},
|
||||
column-gutter: ${gutter}in,
|
||||
row-gutter: ${gutter}in,
|
||||
${numbers
|
||||
.map((_, i) => {
|
||||
return ` image("card_${i}_front.svg", width: ${cardWidth}in, height: ${cardHeight}in, fit: "contain"),`
|
||||
})
|
||||
.join('\n')}
|
||||
)
|
||||
|
||||
// Add preview label
|
||||
#place(
|
||||
top + right,
|
||||
dx: -0.5in,
|
||||
dy: 0.25in,
|
||||
text(10pt, fill: gray)[Preview (first ${numbers.length} cards)]
|
||||
)
|
||||
`
|
||||
|
||||
// Compile with Typst: stdin for .typ content, stdout for SVG output
|
||||
let svg: string
|
||||
try {
|
||||
svg = execSync('typst compile --format svg - -', {
|
||||
input: typstContent,
|
||||
encoding: 'utf8',
|
||||
cwd: tempDir, // Run in temp dir so relative paths work
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Typst compilation error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to compile preview. Is Typst installed?' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
tempDir = null
|
||||
|
||||
return NextResponse.json({ svg })
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error)
|
||||
|
||||
// Clean up temp directory if it exists
|
||||
if (tempDir) {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to clean up temp directory:', cleanupError)
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate preview', message: errorMessage },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// API route for generating compact addition problem examples for display option previews
|
||||
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { execSync } from 'child_process'
|
||||
import { generateProblems } from '@/app/create/worksheets/addition/problemGenerator'
|
||||
import {
|
||||
generateTypstHelpers,
|
||||
generateProblemStackFunction,
|
||||
} from '@/app/create/worksheets/addition/typstHelpers'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface ExampleRequest {
|
||||
showCarryBoxes?: boolean
|
||||
showAnswerBoxes?: boolean
|
||||
showPlaceValueColors?: boolean
|
||||
showProblemNumbers?: boolean
|
||||
showCellBorder?: boolean
|
||||
showTenFrames?: boolean
|
||||
showTenFramesForAll?: boolean
|
||||
fontSize?: number
|
||||
addend1?: number
|
||||
addend2?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single compact problem example showing the combined display options
|
||||
* Uses the EXACT same Typst structure as the full worksheet generator
|
||||
*/
|
||||
function generateExampleTypst(config: ExampleRequest): string {
|
||||
// Use custom addends if provided, otherwise generate a problem
|
||||
let a: number
|
||||
let b: number
|
||||
|
||||
if (config.addend1 !== undefined && config.addend2 !== undefined) {
|
||||
a = config.addend1
|
||||
b = config.addend2
|
||||
} else {
|
||||
// Generate a simple 2-digit + 2-digit problem with carries
|
||||
const problems = generateProblems(1, 0.8, 0.5, false, 12345)
|
||||
const problem = problems[0]
|
||||
a = problem.a
|
||||
b = problem.b
|
||||
}
|
||||
|
||||
const fontSize = config.fontSize || 14
|
||||
const cellSize = 0.35 // Compact cell size for examples
|
||||
|
||||
// Boolean flags matching worksheet generator
|
||||
const showCarries = config.showCarryBoxes ?? false
|
||||
const showAnswers = config.showAnswerBoxes ?? false
|
||||
const showColors = config.showPlaceValueColors ?? false
|
||||
const showNumbers = config.showProblemNumbers ?? false
|
||||
const showTenFrames = config.showTenFrames ?? false
|
||||
const showTenFramesForAll = config.showTenFramesForAll ?? false
|
||||
|
||||
return String.raw`
|
||||
#set page(width: auto, height: auto, margin: 8pt, fill: white)
|
||||
#set text(size: ${fontSize}pt, font: "New Computer Modern Math")
|
||||
|
||||
#let heavy-stroke = 0.8pt
|
||||
#let show-carries = ${showCarries ? 'true' : 'false'}
|
||||
#let show-answers = ${showAnswers ? 'true' : 'false'}
|
||||
#let show-colors = ${showColors ? 'true' : 'false'}
|
||||
#let show-numbers = ${showNumbers ? 'true' : 'false'}
|
||||
#let show-ten-frames = ${showTenFrames ? 'true' : 'false'}
|
||||
#let show-ten-frames-for-all = ${showTenFramesForAll ? 'true' : 'false'}
|
||||
|
||||
${generateTypstHelpers(cellSize)}
|
||||
|
||||
${generateProblemStackFunction(cellSize)}
|
||||
|
||||
#let a = ${a}
|
||||
#let b = ${b}
|
||||
#let aT = calc.floor(calc.rem(a, 100) / 10)
|
||||
#let aO = calc.rem(a, 10)
|
||||
#let bT = calc.floor(calc.rem(b, 100) / 10)
|
||||
#let bO = calc.rem(b, 10)
|
||||
|
||||
#align(center + horizon)[
|
||||
#problem-stack(a, b, aT, aO, bT, bO, if show-numbers { 0 } else { none })
|
||||
]
|
||||
`
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: ExampleRequest = await request.json()
|
||||
|
||||
// Generate Typst source with all display options
|
||||
const typstSource = generateExampleTypst(body)
|
||||
|
||||
// Compile to SVG
|
||||
const svg = execSync('typst compile --format svg - -', {
|
||||
input: typstSource,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
})
|
||||
|
||||
return NextResponse.json({ svg })
|
||||
} catch (error) {
|
||||
console.error('Error generating example:', error)
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to generate example',
|
||||
message: errorMessage,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// API route for generating addition worksheet previews (SVG)
|
||||
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { generateWorksheetPreview } from '@/app/create/worksheets/addition/generatePreview'
|
||||
import type { WorksheetFormState } from '@/app/create/worksheets/addition/types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: WorksheetFormState = await request.json()
|
||||
|
||||
// Generate preview using shared logic
|
||||
const result = generateWorksheetPreview(body)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: result.error,
|
||||
details: result.details,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Return pages as JSON
|
||||
return NextResponse.json({ pages: result.pages })
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error)
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to generate preview',
|
||||
message: errorMessage,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// API route for generating addition worksheets
|
||||
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { execSync } from 'child_process'
|
||||
import { validateWorksheetConfig } from '@/app/create/worksheets/addition/validation'
|
||||
import { generateProblems } from '@/app/create/worksheets/addition/problemGenerator'
|
||||
import { generateTypstSource } from '@/app/create/worksheets/addition/typstGenerator'
|
||||
import type { WorksheetFormState } from '@/app/create/worksheets/addition/types'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: WorksheetFormState = await request.json()
|
||||
|
||||
// Validate configuration
|
||||
const validation = validateWorksheetConfig(body)
|
||||
if (!validation.isValid || !validation.config) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid configuration', errors: validation.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const config = validation.config
|
||||
|
||||
// Generate problems
|
||||
const problems = generateProblems(
|
||||
config.total,
|
||||
config.pAnyStart,
|
||||
config.pAllStart,
|
||||
config.interpolate,
|
||||
config.seed
|
||||
)
|
||||
|
||||
// Generate Typst sources (one per page)
|
||||
const typstSources = generateTypstSource(config, problems)
|
||||
|
||||
// Join pages with pagebreak for PDF
|
||||
const typstSource = typstSources.join('\n\n#pagebreak()\n\n')
|
||||
|
||||
// Compile with Typst: stdin → stdout
|
||||
let pdfBuffer: Buffer
|
||||
try {
|
||||
pdfBuffer = execSync('typst compile --format pdf - -', {
|
||||
input: typstSource,
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB limit
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Typst compilation error:', error)
|
||||
|
||||
// Extract the actual Typst error message
|
||||
const stderr =
|
||||
error instanceof Error && 'stderr' in error
|
||||
? String((error as any).stderr)
|
||||
: 'Unknown compilation error'
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to compile worksheet PDF',
|
||||
details: stderr,
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
typstSource: typstSource.split('\n').slice(0, 20).join('\n') + '\n...',
|
||||
}),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Return binary PDF directly
|
||||
return new Response(pdfBuffer as unknown as BodyInit, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="addition-worksheet-${Date.now()}.pdf"`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating worksheet:', error)
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? error.stack : undefined
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to generate worksheet',
|
||||
message: errorMessage,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: errorStack }),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,6 @@ import { getActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { db, schema } from '@/db'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
// Force dynamic rendering - this route uses headers()
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* GET /api/debug/active-players
|
||||
* Debug endpoint to check active players for current user
|
||||
|
||||
@@ -1,237 +1,68 @@
|
||||
import { SorobanGenerator } from '@soroban/core'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import type { FlashcardConfig } from '@/app/create/flashcards/page'
|
||||
import {
|
||||
generateFlashcardFront,
|
||||
generateFlashcardBack,
|
||||
} from '@/utils/flashcards/generateFlashcardSvgs'
|
||||
import path from 'path'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
// Global generator instance for better performance
|
||||
let generator: SorobanGenerator | null = null
|
||||
|
||||
/**
|
||||
* Parse range string to get all numbers
|
||||
*/
|
||||
function parseRange(range: string, step: number): number[] {
|
||||
const numbers: number[] = []
|
||||
async function getGenerator() {
|
||||
if (!generator) {
|
||||
// Point to the core package in our monorepo
|
||||
const corePackagePath = path.join(process.cwd(), '../../packages/core')
|
||||
generator = new SorobanGenerator(corePackagePath)
|
||||
|
||||
if (range.includes('-')) {
|
||||
const [start, end] = range.split('-').map((n) => parseInt(n, 10))
|
||||
for (let i = start; i <= end; i += step) {
|
||||
numbers.push(i)
|
||||
}
|
||||
} else if (range.includes(',')) {
|
||||
const parts = range.split(',').map((n) => parseInt(n.trim(), 10))
|
||||
numbers.push(...parts)
|
||||
} else {
|
||||
numbers.push(parseInt(range, 10))
|
||||
}
|
||||
|
||||
return numbers
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle array with seed for reproducibility
|
||||
*/
|
||||
function shuffleWithSeed<T>(array: T[], seed?: number): T[] {
|
||||
const shuffled = [...array]
|
||||
const rng = seed !== undefined ? seededRandom(seed) : Math.random
|
||||
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng() * (i + 1))
|
||||
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
|
||||
}
|
||||
|
||||
return shuffled
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple seeded random number generator (Mulberry32)
|
||||
*/
|
||||
function seededRandom(seed: number) {
|
||||
return () => {
|
||||
seed = (seed + 0x6d2b79f5) | 0
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
// Note: SorobanGenerator from @soroban/core doesn't have initialize method
|
||||
// It uses one-shot mode by default
|
||||
}
|
||||
return generator
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let tempDir: string | null = null
|
||||
|
||||
try {
|
||||
const config: FlashcardConfig = await request.json()
|
||||
const {
|
||||
range = '0-99',
|
||||
step = 1,
|
||||
cardsPerPage = 6,
|
||||
paperSize = 'us-letter',
|
||||
orientation = 'portrait',
|
||||
margins,
|
||||
gutter = '5mm',
|
||||
shuffle = false,
|
||||
seed,
|
||||
showCutMarks = false,
|
||||
showRegistration = false,
|
||||
beadShape = 'diamond',
|
||||
colorScheme = 'place-value',
|
||||
colorPalette = 'default',
|
||||
hideInactiveBeads = false,
|
||||
showEmptyColumns = false,
|
||||
columns = 'auto',
|
||||
scaleFactor = 0.9,
|
||||
coloredNumerals = false,
|
||||
format = 'pdf',
|
||||
} = config
|
||||
const config = await request.json()
|
||||
|
||||
// Dynamic import to avoid Next.js bundler issues
|
||||
const { renderToStaticMarkup } = await import('react-dom/server')
|
||||
// Debug: log the received config
|
||||
console.log('📥 Received config:', JSON.stringify(config, null, 2))
|
||||
|
||||
// Create temp directory for SVG files
|
||||
tempDir = join(tmpdir(), `flashcards-${Date.now()}-${Math.random()}`)
|
||||
mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
// Get all numbers
|
||||
let numbers = parseRange(range, step)
|
||||
|
||||
// Apply shuffle if requested
|
||||
if (shuffle) {
|
||||
numbers = shuffleWithSeed(numbers, seed)
|
||||
// Ensure range is set with a default
|
||||
if (!config.range) {
|
||||
console.log('⚠️ No range provided, using default: 0-99')
|
||||
config.range = '0-99'
|
||||
}
|
||||
|
||||
if (numbers.length === 0) {
|
||||
return NextResponse.json({ error: 'No valid numbers in range' }, { status: 400 })
|
||||
}
|
||||
// Get generator instance
|
||||
const gen = await getGenerator()
|
||||
|
||||
// Generate SVG files for each card (front and back)
|
||||
const svgConfig = {
|
||||
beadShape,
|
||||
colorScheme,
|
||||
colorPalette,
|
||||
hideInactiveBeads,
|
||||
showEmptyColumns,
|
||||
columns: columns === 'auto' ? 'auto' : Number(columns),
|
||||
scaleFactor,
|
||||
coloredNumerals,
|
||||
}
|
||||
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
const num = numbers[i]
|
||||
|
||||
// Generate front (abacus)
|
||||
const frontElement = generateFlashcardFront(num, svgConfig)
|
||||
const frontSvg = renderToStaticMarkup(frontElement)
|
||||
writeFileSync(join(tempDir, `card_${i}_front.svg`), frontSvg)
|
||||
|
||||
// Generate back (numeral)
|
||||
const backElement = generateFlashcardBack(num, svgConfig)
|
||||
const backSvg = renderToStaticMarkup(backElement)
|
||||
writeFileSync(join(tempDir, `card_${i}_back.svg`), backSvg)
|
||||
}
|
||||
|
||||
// Calculate paper dimensions and layout
|
||||
const paperDimensions = {
|
||||
'us-letter': { width: 8.5, height: 11 },
|
||||
a4: { width: 8.27, height: 11.69 },
|
||||
a3: { width: 11.69, height: 16.54 },
|
||||
a5: { width: 5.83, height: 8.27 },
|
||||
}
|
||||
|
||||
const paper = paperDimensions[paperSize] || paperDimensions['us-letter']
|
||||
const [pageWidth, pageHeight] =
|
||||
orientation === 'landscape' ? [paper.height, paper.width] : [paper.width, paper.height]
|
||||
|
||||
// Calculate grid layout (typically 2 columns × 3 rows for 6 cards)
|
||||
const cols = 2
|
||||
const rows = Math.ceil(cardsPerPage / cols)
|
||||
|
||||
// Use provided margins or defaults
|
||||
const margin = {
|
||||
top: margins?.top || '0.5in',
|
||||
bottom: margins?.bottom || '0.5in',
|
||||
left: margins?.left || '0.5in',
|
||||
right: margins?.right || '0.5in',
|
||||
}
|
||||
|
||||
// Parse gutter (convert from string like "5mm" to inches for calculation)
|
||||
const gutterInches = parseFloat(gutter) / 25.4 // Rough mm to inch conversion
|
||||
|
||||
// Calculate available space (approximate, Typst will handle exact layout)
|
||||
const marginInches = 0.5 // Simplified for now
|
||||
const availableWidth = pageWidth - 2 * marginInches - gutterInches * (cols - 1)
|
||||
const availableHeight = pageHeight - 2 * marginInches - gutterInches * (rows - 1)
|
||||
const cardWidth = availableWidth / cols
|
||||
const cardHeight = availableHeight / rows
|
||||
|
||||
// Generate pages
|
||||
const totalPages = Math.ceil(numbers.length / cardsPerPage)
|
||||
const pages: string[] = []
|
||||
|
||||
for (let pageNum = 0; pageNum < totalPages; pageNum++) {
|
||||
const startIdx = pageNum * cardsPerPage
|
||||
const endIdx = Math.min(startIdx + cardsPerPage, numbers.length)
|
||||
const pageCards = []
|
||||
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
pageCards.push(
|
||||
` image("card_${i}_front.svg", width: ${cardWidth}in, height: ${cardHeight}in, fit: "contain")`
|
||||
)
|
||||
}
|
||||
|
||||
// Fill remaining slots with empty cells if needed
|
||||
const remaining = cardsPerPage - pageCards.length
|
||||
for (let i = 0; i < remaining; i++) {
|
||||
pageCards.push(` []`) // Empty cell
|
||||
}
|
||||
|
||||
pages.push(`#grid(
|
||||
columns: ${cols},
|
||||
rows: ${rows},
|
||||
column-gutter: ${gutter},
|
||||
row-gutter: ${gutter},
|
||||
${pageCards.join(',\n')}
|
||||
)`)
|
||||
}
|
||||
|
||||
// Generate Typst document
|
||||
const typstContent = `
|
||||
#set page(
|
||||
paper: "${paperSize}",
|
||||
margin: (x: ${margin.left}, y: ${margin.top}),
|
||||
flipped: ${orientation === 'landscape'},
|
||||
)
|
||||
|
||||
${pages.join('\n\n#pagebreak()\n\n')}
|
||||
`
|
||||
|
||||
// Compile with Typst
|
||||
let pdfBuffer: Buffer
|
||||
try {
|
||||
pdfBuffer = execSync('typst compile --format pdf - -', {
|
||||
input: typstContent,
|
||||
cwd: tempDir, // Run in temp dir so relative paths work
|
||||
maxBuffer: 100 * 1024 * 1024, // 100MB limit for large sets
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Typst compilation error:', error)
|
||||
// Check dependencies before generating
|
||||
const deps = await gen.checkDependencies?.()
|
||||
if (deps && (!deps.python || !deps.typst)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to compile PDF. Is Typst installed?' },
|
||||
{
|
||||
error: 'Missing system dependencies',
|
||||
details: {
|
||||
python: deps.python ? '✅ Available' : '❌ Missing Python 3',
|
||||
typst: deps.typst ? '✅ Available' : '❌ Missing Typst',
|
||||
qpdf: deps.qpdf ? '✅ Available' : '⚠️ Missing qpdf (optional)',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
tempDir = null
|
||||
// Generate flashcards using Python via TypeScript bindings
|
||||
console.log('🚀 Generating flashcards with config:', JSON.stringify(config, null, 2))
|
||||
const result = await gen.generate(config)
|
||||
|
||||
// SorobanGenerator.generate() returns PDF data directly as Buffer
|
||||
if (!Buffer.isBuffer(result)) {
|
||||
throw new Error(`Expected PDF Buffer from generator, got: ${typeof result}`)
|
||||
}
|
||||
const pdfBuffer = result
|
||||
// Create filename for download
|
||||
const filename = `soroban-flashcards-${range}.pdf`
|
||||
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
|
||||
|
||||
// Return PDF directly as download
|
||||
return new NextResponse(pdfBuffer, {
|
||||
return new NextResponse(new Uint8Array(pdfBuffer), {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
@@ -239,45 +70,70 @@ ${pages.join('\n\n#pagebreak()\n\n')}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating flashcards:', error)
|
||||
console.error('❌ Generation failed:', error)
|
||||
|
||||
// Clean up temp directory if it exists
|
||||
if (tempDir) {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to clean up temp directory:', cleanupError)
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate flashcards', message: errorMessage },
|
||||
{
|
||||
error: 'Failed to generate flashcards',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to calculate metadata
|
||||
function _calculateCardCount(range: string, step: number): number {
|
||||
if (range.includes('-')) {
|
||||
const [start, end] = range.split('-').map((n) => parseInt(n, 10) || 0)
|
||||
return Math.floor((end - start + 1) / step)
|
||||
}
|
||||
|
||||
if (range.includes(',')) {
|
||||
return range.split(',').length
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function _generateNumbersFromRange(range: string, step: number): number[] {
|
||||
if (range.includes('-')) {
|
||||
const [start, end] = range.split('-').map((n) => parseInt(n, 10) || 0)
|
||||
const numbers: number[] = []
|
||||
for (let i = start; i <= end; i += step) {
|
||||
numbers.push(i)
|
||||
if (numbers.length >= 100) break // Limit to prevent huge arrays
|
||||
}
|
||||
return numbers
|
||||
}
|
||||
|
||||
if (range.includes(',')) {
|
||||
return range.split(',').map((n) => parseInt(n.trim(), 10) || 0)
|
||||
}
|
||||
|
||||
return [parseInt(range, 10) || 0]
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
export async function GET() {
|
||||
try {
|
||||
// Check if Typst is available
|
||||
execSync('typst --version', { encoding: 'utf8' })
|
||||
const gen = await getGenerator()
|
||||
const deps = (await gen.checkDependencies?.()) || {
|
||||
python: true,
|
||||
typst: true,
|
||||
qpdf: true,
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
status: 'healthy',
|
||||
generator: 'typescript-typst',
|
||||
dependencies: {
|
||||
typst: true,
|
||||
python: false, // No longer needed!
|
||||
},
|
||||
dependencies: deps,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'unhealthy',
|
||||
error: 'Typst not available',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/db'
|
||||
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
|
||||
import { playerStats } from '@/db/schema/player-stats'
|
||||
import { players } from '@/db/schema/players'
|
||||
import type { GetPlayerStatsResponse, PlayerStatsData } from '@/lib/arcade/stats/types'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
|
||||
/**
|
||||
* GET /api/player-stats/[playerId]
|
||||
*
|
||||
* Fetches stats for a specific player (must be owned by current user).
|
||||
*/
|
||||
export async function GET(_request: Request, { params }: { params: { playerId: string } }) {
|
||||
try {
|
||||
const { playerId } = params
|
||||
|
||||
// 1. Authenticate user
|
||||
const viewerId = await getViewerId()
|
||||
if (!viewerId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 2. Verify player belongs to user
|
||||
const player = await db
|
||||
.select()
|
||||
.from(players)
|
||||
.where(eq(players.id, playerId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!player) {
|
||||
return NextResponse.json({ error: 'Player not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (player.userId !== viewerId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Forbidden: player belongs to another user' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Fetch player stats
|
||||
const stats = await db
|
||||
.select()
|
||||
.from(playerStats)
|
||||
.where(eq(playerStats.playerId, playerId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
const playerStatsData: PlayerStatsData = stats
|
||||
? convertToPlayerStatsData(stats)
|
||||
: createDefaultPlayerStats(playerId)
|
||||
|
||||
// 4. Return response
|
||||
const response: GetPlayerStatsResponse = {
|
||||
stats: playerStatsData,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch player stats:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch player stats',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DB record to PlayerStatsData
|
||||
*/
|
||||
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
|
||||
return {
|
||||
playerId: dbStats.playerId,
|
||||
gamesPlayed: dbStats.gamesPlayed,
|
||||
totalWins: dbStats.totalWins,
|
||||
totalLosses: dbStats.totalLosses,
|
||||
bestTime: dbStats.bestTime,
|
||||
highestAccuracy: dbStats.highestAccuracy,
|
||||
favoriteGameType: dbStats.favoriteGameType,
|
||||
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
|
||||
lastPlayedAt: dbStats.lastPlayedAt,
|
||||
createdAt: dbStats.createdAt,
|
||||
updatedAt: dbStats.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default player stats for new player
|
||||
*/
|
||||
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
|
||||
const now = new Date()
|
||||
return {
|
||||
playerId,
|
||||
gamesPlayed: 0,
|
||||
totalWins: 0,
|
||||
totalLosses: 0,
|
||||
bestTime: null,
|
||||
highestAccuracy: 0,
|
||||
favoriteGameType: null,
|
||||
gameStats: {},
|
||||
lastPlayedAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/db'
|
||||
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
|
||||
import { playerStats } from '@/db/schema/player-stats'
|
||||
import type {
|
||||
GameResult,
|
||||
PlayerGameResult,
|
||||
PlayerStatsData,
|
||||
RecordGameRequest,
|
||||
RecordGameResponse,
|
||||
StatsUpdate,
|
||||
} from '@/lib/arcade/stats/types'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
|
||||
/**
|
||||
* POST /api/player-stats/record-game
|
||||
*
|
||||
* Records a game result and updates player stats for all participants.
|
||||
* Supports cooperative games (team wins/losses) and competitive games.
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
// 1. Authenticate user
|
||||
const viewerId = await getViewerId()
|
||||
if (!viewerId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 2. Parse and validate request
|
||||
const body: RecordGameRequest = await request.json()
|
||||
const { gameResult } = body
|
||||
|
||||
if (!gameResult || !gameResult.playerResults || gameResult.playerResults.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid game result: playerResults required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!gameResult.gameType) {
|
||||
return NextResponse.json({ error: 'Invalid game result: gameType required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 3. Process each player's result
|
||||
const updates: StatsUpdate[] = []
|
||||
|
||||
for (const playerResult of gameResult.playerResults) {
|
||||
const update = await recordPlayerResult(gameResult, playerResult)
|
||||
updates.push(update)
|
||||
}
|
||||
|
||||
// 4. Return success response
|
||||
const response: RecordGameResponse = {
|
||||
success: true,
|
||||
updates,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to record game result:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to record game result',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records stats for a single player's game result
|
||||
*/
|
||||
async function recordPlayerResult(
|
||||
gameResult: GameResult,
|
||||
playerResult: PlayerGameResult
|
||||
): Promise<StatsUpdate> {
|
||||
const { playerId } = playerResult
|
||||
|
||||
// 1. Fetch or create player stats
|
||||
const existingStats = await db
|
||||
.select()
|
||||
.from(playerStats)
|
||||
.where(eq(playerStats.playerId, playerId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
const previousStats: PlayerStatsData = existingStats
|
||||
? convertToPlayerStatsData(existingStats)
|
||||
: createDefaultPlayerStats(playerId)
|
||||
|
||||
// 2. Calculate new stats
|
||||
const newStats: PlayerStatsData = { ...previousStats }
|
||||
|
||||
// Always increment games played
|
||||
newStats.gamesPlayed++
|
||||
|
||||
// Handle wins/losses (cooperative vs competitive)
|
||||
if (gameResult.metadata?.isTeamVictory !== undefined) {
|
||||
// Cooperative game: all players share outcome
|
||||
if (playerResult.won) {
|
||||
newStats.totalWins++
|
||||
} else {
|
||||
newStats.totalLosses++
|
||||
}
|
||||
} else {
|
||||
// Competitive/Solo: individual outcome
|
||||
if (playerResult.won) {
|
||||
newStats.totalWins++
|
||||
} else {
|
||||
newStats.totalLosses++
|
||||
}
|
||||
}
|
||||
|
||||
// Update best time (if provided and improved)
|
||||
if (playerResult.completionTime) {
|
||||
if (!newStats.bestTime || playerResult.completionTime < newStats.bestTime) {
|
||||
newStats.bestTime = playerResult.completionTime
|
||||
}
|
||||
}
|
||||
|
||||
// Update highest accuracy (if provided and improved)
|
||||
if (playerResult.accuracy !== undefined && playerResult.accuracy > newStats.highestAccuracy) {
|
||||
newStats.highestAccuracy = playerResult.accuracy
|
||||
}
|
||||
|
||||
// Update per-game stats (JSON)
|
||||
const gameType = gameResult.gameType
|
||||
const currentGameStats: GameStatsBreakdown = newStats.gameStats[gameType] || {
|
||||
gamesPlayed: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
bestTime: null,
|
||||
highestAccuracy: 0,
|
||||
averageScore: 0,
|
||||
lastPlayed: 0,
|
||||
}
|
||||
|
||||
currentGameStats.gamesPlayed++
|
||||
if (playerResult.won) {
|
||||
currentGameStats.wins++
|
||||
} else {
|
||||
currentGameStats.losses++
|
||||
}
|
||||
|
||||
// Update game-specific best time
|
||||
if (playerResult.completionTime) {
|
||||
if (!currentGameStats.bestTime || playerResult.completionTime < currentGameStats.bestTime) {
|
||||
currentGameStats.bestTime = playerResult.completionTime
|
||||
}
|
||||
}
|
||||
|
||||
// Update game-specific highest accuracy
|
||||
if (
|
||||
playerResult.accuracy !== undefined &&
|
||||
playerResult.accuracy > currentGameStats.highestAccuracy
|
||||
) {
|
||||
currentGameStats.highestAccuracy = playerResult.accuracy
|
||||
}
|
||||
|
||||
// Update average score
|
||||
if (playerResult.score !== undefined) {
|
||||
const previousTotal = currentGameStats.averageScore * (currentGameStats.gamesPlayed - 1)
|
||||
currentGameStats.averageScore =
|
||||
(previousTotal + playerResult.score) / currentGameStats.gamesPlayed
|
||||
}
|
||||
|
||||
currentGameStats.lastPlayed = gameResult.completedAt
|
||||
|
||||
newStats.gameStats[gameType] = currentGameStats
|
||||
|
||||
// Update favorite game type (most played)
|
||||
newStats.favoriteGameType = getMostPlayedGame(newStats.gameStats)
|
||||
|
||||
// Update timestamps
|
||||
newStats.lastPlayedAt = new Date(gameResult.completedAt)
|
||||
newStats.updatedAt = new Date()
|
||||
|
||||
// 3. Save to database
|
||||
if (existingStats) {
|
||||
// Update existing record
|
||||
await db
|
||||
.update(playerStats)
|
||||
.set({
|
||||
gamesPlayed: newStats.gamesPlayed,
|
||||
totalWins: newStats.totalWins,
|
||||
totalLosses: newStats.totalLosses,
|
||||
bestTime: newStats.bestTime,
|
||||
highestAccuracy: newStats.highestAccuracy,
|
||||
favoriteGameType: newStats.favoriteGameType,
|
||||
gameStats: newStats.gameStats as any, // Drizzle JSON type
|
||||
lastPlayedAt: newStats.lastPlayedAt,
|
||||
updatedAt: newStats.updatedAt,
|
||||
})
|
||||
.where(eq(playerStats.playerId, playerId))
|
||||
} else {
|
||||
// Insert new record
|
||||
await db.insert(playerStats).values({
|
||||
playerId: newStats.playerId,
|
||||
gamesPlayed: newStats.gamesPlayed,
|
||||
totalWins: newStats.totalWins,
|
||||
totalLosses: newStats.totalLosses,
|
||||
bestTime: newStats.bestTime,
|
||||
highestAccuracy: newStats.highestAccuracy,
|
||||
favoriteGameType: newStats.favoriteGameType,
|
||||
gameStats: newStats.gameStats as any,
|
||||
lastPlayedAt: newStats.lastPlayedAt,
|
||||
createdAt: newStats.createdAt,
|
||||
updatedAt: newStats.updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Return update summary
|
||||
return {
|
||||
playerId,
|
||||
previousStats,
|
||||
newStats,
|
||||
changes: {
|
||||
gamesPlayed: newStats.gamesPlayed - previousStats.gamesPlayed,
|
||||
wins: newStats.totalWins - previousStats.totalWins,
|
||||
losses: newStats.totalLosses - previousStats.totalLosses,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DB record to PlayerStatsData
|
||||
*/
|
||||
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
|
||||
return {
|
||||
playerId: dbStats.playerId,
|
||||
gamesPlayed: dbStats.gamesPlayed,
|
||||
totalWins: dbStats.totalWins,
|
||||
totalLosses: dbStats.totalLosses,
|
||||
bestTime: dbStats.bestTime,
|
||||
highestAccuracy: dbStats.highestAccuracy,
|
||||
favoriteGameType: dbStats.favoriteGameType,
|
||||
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
|
||||
lastPlayedAt: dbStats.lastPlayedAt,
|
||||
createdAt: dbStats.createdAt,
|
||||
updatedAt: dbStats.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default player stats for new player
|
||||
*/
|
||||
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
|
||||
const now = new Date()
|
||||
return {
|
||||
playerId,
|
||||
gamesPlayed: 0,
|
||||
totalWins: 0,
|
||||
totalLosses: 0,
|
||||
bestTime: null,
|
||||
highestAccuracy: 0,
|
||||
favoriteGameType: null,
|
||||
gameStats: {},
|
||||
lastPlayedAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine most-played game from game stats
|
||||
*/
|
||||
function getMostPlayedGame(gameStats: Record<string, GameStatsBreakdown>): string | null {
|
||||
const games = Object.entries(gameStats)
|
||||
if (games.length === 0) return null
|
||||
|
||||
return games.reduce((mostPlayed, [gameType, stats]) => {
|
||||
const mostPlayedStats = gameStats[mostPlayed]
|
||||
return stats.gamesPlayed > (mostPlayedStats?.gamesPlayed || 0) ? gameType : mostPlayed
|
||||
}, games[0][0])
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/db'
|
||||
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
|
||||
import { playerStats } from '@/db/schema/player-stats'
|
||||
import { players } from '@/db/schema/players'
|
||||
import type { GetAllPlayerStatsResponse, PlayerStatsData } from '@/lib/arcade/stats/types'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
|
||||
// Force dynamic rendering - this route uses headers()
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* GET /api/player-stats
|
||||
*
|
||||
* Fetches stats for all of the current user's players.
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
// 1. Authenticate user
|
||||
const viewerId = await getViewerId()
|
||||
if (!viewerId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 2. Fetch all user's players
|
||||
const userPlayers = await db.select().from(players).where(eq(players.userId, viewerId))
|
||||
|
||||
const playerIds = userPlayers.map((p) => p.id)
|
||||
|
||||
// 3. Fetch stats for all players
|
||||
const allStats: PlayerStatsData[] = []
|
||||
|
||||
for (const playerId of playerIds) {
|
||||
const stats = await db
|
||||
.select()
|
||||
.from(playerStats)
|
||||
.where(eq(playerStats.playerId, playerId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (stats) {
|
||||
allStats.push(convertToPlayerStatsData(stats))
|
||||
} else {
|
||||
// Player exists but has no stats yet
|
||||
allStats.push(createDefaultPlayerStats(playerId))
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Return response
|
||||
const response: GetAllPlayerStatsResponse = {
|
||||
playerStats: allStats,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch player stats:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch player stats',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DB record to PlayerStatsData
|
||||
*/
|
||||
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
|
||||
return {
|
||||
playerId: dbStats.playerId,
|
||||
gamesPlayed: dbStats.gamesPlayed,
|
||||
totalWins: dbStats.totalWins,
|
||||
totalLosses: dbStats.totalLosses,
|
||||
bestTime: dbStats.bestTime,
|
||||
highestAccuracy: dbStats.highestAccuracy,
|
||||
favoriteGameType: dbStats.favoriteGameType,
|
||||
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
|
||||
lastPlayedAt: dbStats.lastPlayedAt,
|
||||
createdAt: dbStats.createdAt,
|
||||
updatedAt: dbStats.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default player stats for new player
|
||||
*/
|
||||
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
|
||||
const now = new Date()
|
||||
return {
|
||||
playerId,
|
||||
gamesPlayed: 0,
|
||||
totalWins: 0,
|
||||
totalLosses: 0,
|
||||
bestTime: null,
|
||||
highestAccuracy: 0,
|
||||
favoriteGameType: null,
|
||||
gameStats: {},
|
||||
lastPlayedAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { db, schema } from '@/db'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import {
|
||||
parseAdditionConfig,
|
||||
serializeAdditionConfig,
|
||||
defaultAdditionConfig,
|
||||
type AdditionConfigV1,
|
||||
} from '@/app/create/worksheets/config-schemas'
|
||||
|
||||
/**
|
||||
* GET /api/worksheets/settings?type=addition
|
||||
* Load user's saved worksheet settings
|
||||
*
|
||||
* Query params:
|
||||
* - type: 'addition' | 'subtraction' | etc.
|
||||
*
|
||||
* Returns:
|
||||
* - config: Parsed and validated config (latest version)
|
||||
* - exists: boolean (true if user has saved settings)
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const viewerId = await getViewerId()
|
||||
const { searchParams } = new URL(req.url)
|
||||
const worksheetType = searchParams.get('type')
|
||||
|
||||
if (!worksheetType) {
|
||||
return NextResponse.json({ error: 'Missing type parameter' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Only 'addition' is supported for now
|
||||
if (worksheetType !== 'addition') {
|
||||
return NextResponse.json(
|
||||
{ error: `Unsupported worksheet type: ${worksheetType}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Look up user's saved settings
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(schema.worksheetSettings)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.worksheetSettings.userId, viewerId),
|
||||
eq(schema.worksheetSettings.worksheetType, worksheetType)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!row) {
|
||||
// No saved settings, return defaults
|
||||
return NextResponse.json({
|
||||
config: defaultAdditionConfig,
|
||||
exists: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Parse and validate config (auto-migrates to latest version)
|
||||
const config = parseAdditionConfig(row.config)
|
||||
|
||||
return NextResponse.json({
|
||||
config,
|
||||
exists: true,
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load worksheet settings:', error)
|
||||
return NextResponse.json({ error: 'Failed to load worksheet settings' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/worksheets/settings
|
||||
* Save user's worksheet settings
|
||||
*
|
||||
* Body:
|
||||
* - type: 'addition' | 'subtraction' | etc.
|
||||
* - config: Config object (version will be added automatically)
|
||||
*
|
||||
* Returns:
|
||||
* - success: boolean
|
||||
* - id: string (worksheet_settings row id)
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
const { type: worksheetType, config } = body
|
||||
|
||||
if (!worksheetType) {
|
||||
return NextResponse.json({ error: 'Missing type field' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return NextResponse.json({ error: 'Missing config field' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Only 'addition' is supported for now
|
||||
if (worksheetType !== 'addition') {
|
||||
return NextResponse.json(
|
||||
{ error: `Unsupported worksheet type: ${worksheetType}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Serialize config (adds version automatically)
|
||||
const configJson = serializeAdditionConfig(config)
|
||||
|
||||
// Check if user already has settings for this type
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(schema.worksheetSettings)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.worksheetSettings.userId, viewerId),
|
||||
eq(schema.worksheetSettings.worksheetType, worksheetType)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const now = new Date()
|
||||
|
||||
if (existing) {
|
||||
// Update existing row
|
||||
await db
|
||||
.update(schema.worksheetSettings)
|
||||
.set({
|
||||
config: configJson,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(schema.worksheetSettings.id, existing.id))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id: existing.id,
|
||||
})
|
||||
} else {
|
||||
// Insert new row
|
||||
const id = crypto.randomUUID()
|
||||
await db.insert(schema.worksheetSettings).values({
|
||||
id,
|
||||
userId: viewerId,
|
||||
worksheetType,
|
||||
config: configJson,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save worksheet settings:', error)
|
||||
return NextResponse.json({ error: 'Failed to save worksheet settings' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ export function AbacusTarget({ number }: AbacusTargetProps) {
|
||||
<AbacusReact
|
||||
value={number}
|
||||
columns={1}
|
||||
compact={true}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={true}
|
||||
scaleFactor={0.72}
|
||||
customStyles={{
|
||||
columnPosts: { opacity: 0 },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,16 +2,12 @@
|
||||
|
||||
import { animated, useSpring } from '@react-spring/web'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { useAbacusSettings } from '@/hooks/useAbacusSettings'
|
||||
|
||||
interface PressureGaugeProps {
|
||||
pressure: number // 0-150 PSI
|
||||
}
|
||||
|
||||
export function PressureGauge({ pressure }: PressureGaugeProps) {
|
||||
// Get native abacus numbers setting
|
||||
const { data: abacusSettings } = useAbacusSettings()
|
||||
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
|
||||
const maxPressure = 150
|
||||
|
||||
// Animate pressure value smoothly with spring physics
|
||||
@@ -111,29 +107,17 @@ export function PressureGauge({ pressure }: PressureGaugeProps) {
|
||||
lineHeight: 0,
|
||||
}}
|
||||
>
|
||||
{useNativeAbacusNumbers ? (
|
||||
<AbacusReact
|
||||
value={psi}
|
||||
columns={3}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={false}
|
||||
scaleFactor={0.6}
|
||||
customStyles={{
|
||||
columnPosts: { opacity: 0 },
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: '#1f2937',
|
||||
}}
|
||||
>
|
||||
{psi}
|
||||
</div>
|
||||
)}
|
||||
<AbacusReact
|
||||
value={psi}
|
||||
columns={3}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={false}
|
||||
scaleFactor={0.6}
|
||||
customStyles={{
|
||||
columnPosts: { opacity: 0 },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
@@ -158,7 +142,7 @@ export function PressureGauge({ pressure }: PressureGaugeProps) {
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Pressure readout */}
|
||||
{/* Abacus readout */}
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
@@ -169,35 +153,27 @@ export function PressureGauge({ pressure }: PressureGaugeProps) {
|
||||
minHeight: '32px',
|
||||
}}
|
||||
>
|
||||
{useNativeAbacusNumbers ? (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 0,
|
||||
}}
|
||||
>
|
||||
<AbacusReact
|
||||
value={Math.round(pressure)}
|
||||
columns={3}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={true}
|
||||
scaleFactor={0.35}
|
||||
customStyles={{
|
||||
columnPosts: { opacity: 0 },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span style={{ fontSize: '12px', color: '#6b7280', fontWeight: 'bold' }}>PSI</span>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#1f2937' }}>
|
||||
{Math.round(pressure)} <span style={{ fontSize: '12px', color: '#6b7280' }}>PSI</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 0,
|
||||
}}
|
||||
>
|
||||
<AbacusReact
|
||||
value={Math.round(pressure)}
|
||||
columns={3}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
hideInactiveBeads={true}
|
||||
scaleFactor={0.35}
|
||||
customStyles={{
|
||||
columnPosts: { opacity: 0 },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span style={{ fontSize: '12px', color: '#6b7280', fontWeight: 'bold' }}>PSI</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* Test to reproduce delivery thrashing bug
|
||||
*
|
||||
* The bug: When a car is at a station for multiple frames (50ms intervals),
|
||||
* the delivery logic fires repeatedly before the optimistic state update propagates.
|
||||
* This causes multiple DELIVER_PASSENGER moves to be sent to the server,
|
||||
* which rejects all but the first one.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
interface Passenger {
|
||||
id: string
|
||||
name: string
|
||||
claimedBy: string | null
|
||||
deliveredBy: string | null
|
||||
carIndex: number | null
|
||||
destinationStationId: string
|
||||
isUrgent: boolean
|
||||
}
|
||||
|
||||
interface Station {
|
||||
id: string
|
||||
name: string
|
||||
emoji: string
|
||||
position: number
|
||||
}
|
||||
|
||||
describe('useSteamJourney - Delivery Thrashing Reproduction', () => {
|
||||
const CAR_SPACING = 7
|
||||
|
||||
/**
|
||||
* Simulate the delivery logic from useSteamJourney
|
||||
* Returns the number of delivery attempts made
|
||||
*/
|
||||
function simulateDeliveryAtPosition(
|
||||
trainPosition: number,
|
||||
passengers: Passenger[],
|
||||
stations: Station[],
|
||||
pendingDeliveryRef: Set<string>
|
||||
): { deliveryAttempts: number; deliveredPassengerIds: string[] } {
|
||||
let deliveryAttempts = 0
|
||||
const deliveredPassengerIds: string[] = []
|
||||
|
||||
const currentBoardedPassengers = passengers.filter(
|
||||
(p) => p.claimedBy !== null && p.deliveredBy === null
|
||||
)
|
||||
|
||||
// PRIORITY 1: Process deliveries FIRST (dispatch DELIVER moves before BOARD moves)
|
||||
currentBoardedPassengers.forEach((passenger) => {
|
||||
if (!passenger || passenger.deliveredBy !== null || passenger.carIndex === null) return
|
||||
|
||||
// Skip if already has a pending delivery request
|
||||
if (pendingDeliveryRef.has(passenger.id)) return
|
||||
|
||||
const station = stations.find((s) => s.id === passenger.destinationStationId)
|
||||
if (!station) return
|
||||
|
||||
// Calculate this passenger's car position using PHYSICAL carIndex
|
||||
const carPosition = Math.max(0, trainPosition - (passenger.carIndex + 1) * CAR_SPACING)
|
||||
const distance = Math.abs(carPosition - station.position)
|
||||
|
||||
// If this car is at the destination station (within 5% tolerance), deliver
|
||||
if (distance < 5) {
|
||||
// Mark as pending BEFORE dispatch to prevent duplicate delivery attempts across frames
|
||||
pendingDeliveryRef.add(passenger.id)
|
||||
deliveryAttempts++
|
||||
deliveredPassengerIds.push(passenger.id)
|
||||
}
|
||||
})
|
||||
|
||||
return { deliveryAttempts, deliveredPassengerIds }
|
||||
}
|
||||
|
||||
test('WITHOUT fix: multiple frames at same position cause thrashing', () => {
|
||||
const stations: Station[] = [
|
||||
{ id: 's1', name: 'Start', emoji: '🏠', position: 20 },
|
||||
{ id: 's2', name: 'Middle', emoji: '🏢', position: 40 },
|
||||
{ id: 's3', name: 'End', emoji: '🏁', position: 80 },
|
||||
]
|
||||
|
||||
// Passenger "Bob" is in Car 1, heading to station s2 at position 40
|
||||
const passengers: Passenger[] = [
|
||||
{
|
||||
id: 'bob',
|
||||
name: 'Bob',
|
||||
claimedBy: 'player1',
|
||||
deliveredBy: null, // Not yet delivered
|
||||
carIndex: 1, // In car 1 (second car)
|
||||
destinationStationId: 's2',
|
||||
isUrgent: false,
|
||||
},
|
||||
]
|
||||
|
||||
// NO pending delivery tracking (simulating the bug)
|
||||
const noPendingRef = new Set<string>()
|
||||
|
||||
// Train position where Car 1 is at station s2 (position 40)
|
||||
// Car 1 position = trainPosition - (carIndex + 1) * CAR_SPACING
|
||||
// Car 1 position = trainPosition - 2 * 7 = trainPosition - 14
|
||||
// For Car 1 to be at position 40: trainPosition = 40 + 14 = 54
|
||||
const trainPosition = 53.9
|
||||
|
||||
const carPosition = Math.max(0, trainPosition - (1 + 1) * CAR_SPACING)
|
||||
console.log(`Train at ${trainPosition}, Car 1 at ${carPosition}, Station at 40`)
|
||||
expect(Math.abs(carPosition - 40)).toBeLessThan(5) // Verify we're in delivery range
|
||||
|
||||
let totalAttempts = 0
|
||||
|
||||
// Simulate 10 frames (50ms each = 500ms total) at the same position
|
||||
// This mimics what happens when the train is near/at a station
|
||||
for (let frame = 0; frame < 10; frame++) {
|
||||
const result = simulateDeliveryAtPosition(trainPosition, passengers, stations, noPendingRef)
|
||||
totalAttempts += result.deliveryAttempts
|
||||
|
||||
// WITHOUT the pendingDeliveryRef fix, every frame triggers a delivery attempt
|
||||
// because the optimistic update hasn't propagated yet
|
||||
}
|
||||
|
||||
// Without the fix, we expect 10 delivery attempts (one per frame)
|
||||
// because nothing prevents duplicate attempts
|
||||
console.log(`Total delivery attempts without fix: ${totalAttempts}`)
|
||||
expect(totalAttempts).toBe(10) // This demonstrates the bug!
|
||||
})
|
||||
|
||||
test('WITH fix: pendingDeliveryRef prevents thrashing', () => {
|
||||
const stations: Station[] = [
|
||||
{ id: 's1', name: 'Start', emoji: '🏠', position: 20 },
|
||||
{ id: 's2', name: 'Middle', emoji: '🏢', position: 40 },
|
||||
{ id: 's3', name: 'End', emoji: '🏁', position: 80 },
|
||||
]
|
||||
|
||||
const passengers: Passenger[] = [
|
||||
{
|
||||
id: 'bob',
|
||||
name: 'Bob',
|
||||
claimedBy: 'player1',
|
||||
deliveredBy: null,
|
||||
carIndex: 1,
|
||||
destinationStationId: 's2',
|
||||
isUrgent: false,
|
||||
},
|
||||
]
|
||||
|
||||
// WITH pending delivery tracking (the fix)
|
||||
const pendingDeliveryRef = new Set<string>()
|
||||
|
||||
const trainPosition = 53.9
|
||||
|
||||
let totalAttempts = 0
|
||||
|
||||
// Simulate 10 frames at the same position
|
||||
for (let frame = 0; frame < 10; frame++) {
|
||||
const result = simulateDeliveryAtPosition(
|
||||
trainPosition,
|
||||
passengers,
|
||||
stations,
|
||||
pendingDeliveryRef
|
||||
)
|
||||
totalAttempts += result.deliveryAttempts
|
||||
}
|
||||
|
||||
// With the fix, only the FIRST frame should attempt delivery
|
||||
// All subsequent frames skip because passenger.id is in pendingDeliveryRef
|
||||
console.log(`Total delivery attempts with fix: ${totalAttempts}`)
|
||||
expect(totalAttempts).toBe(1) // Only one attempt! ✅
|
||||
})
|
||||
|
||||
test('EDGE CASE: multiple passengers at same station', () => {
|
||||
const stations: Station[] = [
|
||||
{ id: 's1', name: 'Start', emoji: '🏠', position: 20 },
|
||||
{ id: 's2', name: 'Middle', emoji: '🏢', position: 40 },
|
||||
{ id: 's3', name: 'End', emoji: '🏁', position: 80 },
|
||||
]
|
||||
|
||||
// Two passengers in different cars, both going to station s2
|
||||
const passengers: Passenger[] = [
|
||||
{
|
||||
id: 'alice',
|
||||
name: 'Alice',
|
||||
claimedBy: 'player1',
|
||||
deliveredBy: null,
|
||||
carIndex: 0, // Car 0
|
||||
destinationStationId: 's2',
|
||||
isUrgent: false,
|
||||
},
|
||||
{
|
||||
id: 'bob',
|
||||
name: 'Bob',
|
||||
claimedBy: 'player1',
|
||||
deliveredBy: null,
|
||||
carIndex: 1, // Car 1
|
||||
destinationStationId: 's2',
|
||||
isUrgent: false,
|
||||
},
|
||||
]
|
||||
|
||||
const pendingDeliveryRef = new Set<string>()
|
||||
|
||||
// Position where both cars are near station s2 (position 40)
|
||||
// Car 0 at position 40: trainPosition = 40 + 7 = 47
|
||||
// Car 1 at position 40: trainPosition = 40 + 14 = 54
|
||||
// Let's use 50 so Car 0 is at 43 and Car 1 is at 36 (both within 5 of 40)
|
||||
const trainPosition = 46.5
|
||||
|
||||
// Debug: Check car positions
|
||||
const car0Pos = Math.max(0, trainPosition - (0 + 1) * CAR_SPACING)
|
||||
const car1Pos = Math.max(0, trainPosition - (1 + 1) * CAR_SPACING)
|
||||
console.log(
|
||||
`Train at ${trainPosition}, Car 0 at ${car0Pos} (dist ${Math.abs(car0Pos - 40)}), Car 1 at ${car1Pos} (dist ${Math.abs(car1Pos - 40)})`
|
||||
)
|
||||
|
||||
let totalAttempts = 0
|
||||
|
||||
// Simulate 5 frames
|
||||
for (let frame = 0; frame < 5; frame++) {
|
||||
const result = simulateDeliveryAtPosition(
|
||||
trainPosition,
|
||||
passengers,
|
||||
stations,
|
||||
pendingDeliveryRef
|
||||
)
|
||||
totalAttempts += result.deliveryAttempts
|
||||
if (result.deliveryAttempts > 0) {
|
||||
console.log(`Frame ${frame}: Delivered ${result.deliveredPassengerIds.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Should deliver BOTH passengers exactly once (2 total attempts)
|
||||
console.log(`Total delivery attempts for 2 passengers: ${totalAttempts}`)
|
||||
expect(totalAttempts).toBe(2) // Alice once, Bob once ✅
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useContext, useRef } from 'react'
|
||||
import { PreviewModeContext } from '@/components/GamePreview'
|
||||
import { useCallback, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Web Audio API sound effects system
|
||||
@@ -16,7 +15,6 @@ interface Note {
|
||||
|
||||
export function useSoundEffects() {
|
||||
const audioContextsRef = useRef<AudioContext[]>([])
|
||||
const previewMode = useContext(PreviewModeContext)
|
||||
|
||||
/**
|
||||
* Helper function to play multi-note 90s arcade sounds
|
||||
@@ -109,11 +107,6 @@ export function useSoundEffects() {
|
||||
| 'steam_hiss',
|
||||
volume: number = 0.15
|
||||
) => {
|
||||
// Disable all audio in preview mode
|
||||
if (previewMode?.isPreview) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
|
||||
@@ -445,7 +438,7 @@ export function useSoundEffects() {
|
||||
console.log('🎵 Web Audio not supported - missing out on rad 90s sounds!')
|
||||
}
|
||||
},
|
||||
[play90sSound, previewMode]
|
||||
[play90sSound]
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,7 @@ const MOMENTUM_DECAY_RATES = {
|
||||
|
||||
const MOMENTUM_GAIN_PER_CORRECT = 15 // Momentum added for each correct answer
|
||||
const SPEED_MULTIPLIER = 0.15 // Convert momentum to speed (% per second at momentum=100)
|
||||
const UPDATE_INTERVAL = 50 // Update every 50ms (~20 fps)
|
||||
const UPDATE_INTERVAL = 16 // Update every 16ms (~60 fps for smooth animation)
|
||||
const GAME_DURATION = 60000 // 60 seconds in milliseconds
|
||||
|
||||
export function useSteamJourney() {
|
||||
@@ -45,7 +45,6 @@ export function useSteamJourney() {
|
||||
const routeExitThresholdRef = useRef<number>(107) // Default for 1 car: 100 + 7
|
||||
const missedPassengersRef = useRef<Set<string>>(new Set()) // Track which passengers have been logged as missed
|
||||
const pendingBoardingRef = useRef<Set<string>>(new Set()) // Track passengers with pending boarding requests across frames
|
||||
const pendingDeliveryRef = useRef<Set<string>>(new Set()) // Track passengers with pending delivery requests across frames
|
||||
const previousTrainPositionRef = useRef<number>(0) // Track previous position to detect threshold crossings
|
||||
|
||||
// Initialize game start time
|
||||
@@ -66,10 +65,9 @@ export function useSteamJourney() {
|
||||
}
|
||||
}, [state.currentRoute, state.passengers, state.stations, state.maxConcurrentPassengers])
|
||||
|
||||
// Clean up pendingBoardingRef when passengers are claimed/delivered
|
||||
// NOTE: We do NOT clean up pendingDeliveryRef here because delivery should only happen once per route
|
||||
// Clean up pendingBoardingRef when passengers are claimed/delivered or route changes
|
||||
useEffect(() => {
|
||||
// Remove passengers from pending boarding set if they've been claimed or delivered
|
||||
// Remove passengers from pending set if they've been claimed or delivered
|
||||
state.passengers.forEach((passenger) => {
|
||||
if (passenger.claimedBy !== null || passenger.deliveredBy !== null) {
|
||||
pendingBoardingRef.current.delete(passenger.id)
|
||||
@@ -77,10 +75,9 @@ export function useSteamJourney() {
|
||||
})
|
||||
}, [state.passengers])
|
||||
|
||||
// Clear all pending boarding and delivery requests when route changes
|
||||
// Clear all pending boarding requests when route changes
|
||||
useEffect(() => {
|
||||
pendingBoardingRef.current.clear()
|
||||
pendingDeliveryRef.current.clear()
|
||||
missedPassengersRef.current.clear()
|
||||
previousTrainPositionRef.current = 0 // Reset previous position for new route
|
||||
}, [state.currentRoute])
|
||||
@@ -162,9 +159,6 @@ export function useSteamJourney() {
|
||||
currentBoardedPassengers.forEach((passenger) => {
|
||||
if (!passenger || passenger.deliveredBy !== null || passenger.carIndex === null) return
|
||||
|
||||
// Skip if already has a pending delivery request
|
||||
if (pendingDeliveryRef.current.has(passenger.id)) return
|
||||
|
||||
const station = state.stations.find((s) => s.id === passenger.destinationStationId)
|
||||
if (!station) return
|
||||
|
||||
@@ -178,10 +172,6 @@ export function useSteamJourney() {
|
||||
console.log(
|
||||
`🎯 DELIVERY: ${passenger.name} delivered from Car ${passenger.carIndex} to ${station.emoji} ${station.name} (+${points} pts) (trainPos=${trainPosition.toFixed(1)}, carPos=${carPosition.toFixed(1)}, stationPos=${station.position})`
|
||||
)
|
||||
|
||||
// Mark as pending BEFORE dispatch to prevent duplicate delivery attempts across frames
|
||||
pendingDeliveryRef.current.add(passenger.id)
|
||||
|
||||
dispatch({
|
||||
type: 'DELIVER_PASSENGER',
|
||||
passengerId: passenger.id,
|
||||
|
||||
@@ -104,7 +104,9 @@ export function useTrackManagement({
|
||||
setDisplayPassengers(passengers)
|
||||
}
|
||||
// Otherwise, keep displaying old passengers until train resets
|
||||
}, [passengers, displayPassengers, trainPosition, currentRoute])
|
||||
// Note: displayPassengers is intentionally NOT in deps to avoid infinite loop
|
||||
// (it's used for comparison, but we don't need to re-run when it changes)
|
||||
}, [passengers, trainPosition, currentRoute])
|
||||
|
||||
// Generate ties and rails when path is ready
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRoomData, useSetRoomGame, useCreateRoom } from '@/hooks/useRoomData'
|
||||
import { useState } from 'react'
|
||||
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
|
||||
import { useViewerId } from '@/hooks/useViewerId'
|
||||
import { GAMES_CONFIG } from '@/components/GameSelector'
|
||||
import type { GameType } from '@/components/GameSelector'
|
||||
@@ -17,51 +17,20 @@ import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
|
||||
* Shows game selection when no game is set, then shows the game itself once selected.
|
||||
* URL never changes - it's always /arcade regardless of selection, setup, or gameplay.
|
||||
*
|
||||
* Auto-creates a solo room if the user doesn't have one, ensuring they always have
|
||||
* a context in which to play games.
|
||||
* Note: We show a friendly message with a link if no room exists to avoid navigation loops.
|
||||
*
|
||||
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
|
||||
* so we don't need to render it here.
|
||||
*
|
||||
* Test: Verifying compose-updater automatic deployment cycle
|
||||
*/
|
||||
export default function RoomPage() {
|
||||
const router = useRouter()
|
||||
const { roomData, isLoading } = useRoomData()
|
||||
const { data: viewerId } = useViewerId()
|
||||
const { mutate: setRoomGame } = useSetRoomGame()
|
||||
const { mutate: createRoom, isPending: isCreatingRoom } = useCreateRoom()
|
||||
const [permissionError, setPermissionError] = useState<string | null>(null)
|
||||
|
||||
// Auto-create room when user has no room
|
||||
// This happens when:
|
||||
// 1. First time visiting /arcade
|
||||
// 2. After leaving a room
|
||||
useEffect(() => {
|
||||
if (!isLoading && !roomData && viewerId && !isCreatingRoom) {
|
||||
console.log('[RoomPage] No room found, auto-creating room for user:', viewerId)
|
||||
|
||||
createRoom(
|
||||
{
|
||||
name: 'My Room',
|
||||
gameName: null, // No game selected yet
|
||||
gameConfig: undefined, // No game config since no game selected
|
||||
accessMode: 'open' as const, // Open by default - user can change settings later
|
||||
},
|
||||
{
|
||||
onSuccess: (newRoom) => {
|
||||
console.log('[RoomPage] Successfully created room:', newRoom.id)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[RoomPage] Failed to auto-create room:', error)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [isLoading, roomData, viewerId, isCreatingRoom, createRoom])
|
||||
|
||||
// Show loading state (includes both initial load and room creation)
|
||||
if (isLoading || isCreatingRoom) {
|
||||
// Show loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -73,13 +42,12 @@ export default function RoomPage() {
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{isCreatingRoom ? 'Creating solo room...' : 'Loading room...'}
|
||||
Loading room...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If still no room after loading and creation attempt, show fallback
|
||||
// This should rarely happen (only if auto-creation fails)
|
||||
// Show error if no room (instead of redirecting)
|
||||
if (!roomData) {
|
||||
return (
|
||||
<div
|
||||
@@ -94,8 +62,16 @@ export default function RoomPage() {
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<div>Unable to create room</div>
|
||||
<div style={{ fontSize: '14px', color: '#999' }}>Please try refreshing the page</div>
|
||||
<div>No active room found</div>
|
||||
<a
|
||||
href="/arcade"
|
||||
style={{
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
Go to Champion Arena
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PlayingGuideModal } from '@/arcade-games/rithmomachia/components/PlayingGuideModal'
|
||||
|
||||
export default function RithmomachiaGuidePage() {
|
||||
// Guide is always open in this standalone page
|
||||
const [isOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
background: '#f3f4f6',
|
||||
}}
|
||||
>
|
||||
<PlayingGuideModal isOpen={isOpen} onClose={() => window.close()} standalone={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { rithmomachiaGame } from '@/arcade-games/rithmomachia'
|
||||
|
||||
// Force dynamic rendering to avoid build-time initialization errors
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const { Provider, GameComponent } = rithmomachiaGame
|
||||
|
||||
export default function RithmomachiaPage() {
|
||||
return (
|
||||
<Provider>
|
||||
<GameComponent />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { getPostBySlug, getAllPostSlugs } from '@/lib/blog'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
|
||||
interface Props {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
|
||||
// Generate static params for all blog posts
|
||||
export async function generateStaticParams() {
|
||||
const slugs = getAllPostSlugs()
|
||||
return slugs.map((slug) => ({ slug }))
|
||||
}
|
||||
|
||||
// Generate metadata for SEO
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const post = await getPostBySlug(params.slug)
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://abaci.one'
|
||||
const postUrl = `${siteUrl}/blog/${params.slug}`
|
||||
|
||||
return {
|
||||
title: `${post.title} | Abaci.one Blog`,
|
||||
description: post.description,
|
||||
authors: [{ name: post.author }],
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
url: postUrl,
|
||||
siteName: 'Abaci.one',
|
||||
type: 'article',
|
||||
publishedTime: post.publishedAt,
|
||||
modifiedTime: post.updatedAt,
|
||||
authors: [post.author],
|
||||
tags: post.tags,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
},
|
||||
alternates: {
|
||||
canonical: postUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BlogPost({ params }: Props) {
|
||||
let post
|
||||
try {
|
||||
post = await getPostBySlug(params.slug)
|
||||
} catch {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
const updatedDate = new Date(post.updatedAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
const showUpdatedDate = post.publishedAt !== post.updatedAt
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="blog-post-page"
|
||||
className={css({
|
||||
minH: '100vh',
|
||||
bg: 'gray.900',
|
||||
pt: 'var(--app-nav-height-full)',
|
||||
})}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
opacity: 0.05,
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
|
||||
backgroundSize: '40px 40px',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
maxW: '48rem',
|
||||
mx: 'auto',
|
||||
px: { base: '1rem', md: '2rem' },
|
||||
py: { base: '2rem', md: '4rem' },
|
||||
})}
|
||||
>
|
||||
{/* Back link */}
|
||||
<Link
|
||||
href="/blog"
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
mb: '2rem',
|
||||
color: 'rgba(196, 181, 253, 0.8)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '500',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.2s',
|
||||
_hover: {
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span>←</span>
|
||||
<span>Back to Blog</span>
|
||||
</Link>
|
||||
|
||||
{/* Article */}
|
||||
<article data-element="blog-article">
|
||||
<header
|
||||
data-section="article-header"
|
||||
className={css({
|
||||
mb: '3rem',
|
||||
pb: '2rem',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'rgba(75, 85, 99, 0.5)',
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '2rem', md: '2.5rem', lg: '3rem' },
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '1.2',
|
||||
mb: '1rem',
|
||||
color: 'white',
|
||||
})}
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: '1.125rem', md: '1.25rem' },
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
lineHeight: '1.6',
|
||||
mb: '1.5rem',
|
||||
})}
|
||||
>
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
<div
|
||||
data-element="article-meta"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.875rem',
|
||||
color: 'rgba(196, 181, 253, 0.8)',
|
||||
})}
|
||||
>
|
||||
<span data-element="author">{post.author}</span>
|
||||
<span>•</span>
|
||||
<time dateTime={post.publishedAt}>{publishedDate}</time>
|
||||
{showUpdatedDate && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>Updated: {updatedDate}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{post.tags.length > 0 && (
|
||||
<div
|
||||
data-element="tags"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
mt: '1rem',
|
||||
})}
|
||||
>
|
||||
{post.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={css({
|
||||
px: '0.75rem',
|
||||
py: '0.25rem',
|
||||
bg: 'rgba(139, 92, 246, 0.2)',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '500',
|
||||
})}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Article Content */}
|
||||
<div
|
||||
data-section="article-content"
|
||||
className={css({
|
||||
fontSize: { base: '1rem', md: '1.125rem' },
|
||||
lineHeight: '1.75',
|
||||
color: 'rgba(229, 231, 235, 0.95)',
|
||||
|
||||
// Typography styles for markdown content
|
||||
'& h1': {
|
||||
fontSize: { base: '1.875rem', md: '2.25rem' },
|
||||
fontWeight: 'bold',
|
||||
mt: '2.5rem',
|
||||
mb: '1rem',
|
||||
lineHeight: '1.25',
|
||||
color: 'white',
|
||||
},
|
||||
'& h2': {
|
||||
fontSize: { base: '1.5rem', md: '1.875rem' },
|
||||
fontWeight: 'bold',
|
||||
mt: '2rem',
|
||||
mb: '0.875rem',
|
||||
lineHeight: '1.3',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
},
|
||||
'& h3': {
|
||||
fontSize: { base: '1.25rem', md: '1.5rem' },
|
||||
fontWeight: '600',
|
||||
mt: '1.75rem',
|
||||
mb: '0.75rem',
|
||||
lineHeight: '1.4',
|
||||
color: 'rgba(196, 181, 253, 0.9)',
|
||||
},
|
||||
'& p': {
|
||||
mb: '1.25rem',
|
||||
},
|
||||
'& strong': {
|
||||
fontWeight: '600',
|
||||
color: 'white',
|
||||
},
|
||||
'& a': {
|
||||
color: 'rgba(147, 197, 253, 1)',
|
||||
textDecoration: 'underline',
|
||||
_hover: {
|
||||
color: 'rgba(59, 130, 246, 1)',
|
||||
},
|
||||
},
|
||||
'& ul, & ol': {
|
||||
pl: '1.5rem',
|
||||
mb: '1.25rem',
|
||||
},
|
||||
'& li': {
|
||||
mb: '0.5rem',
|
||||
},
|
||||
'& code': {
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
px: '0.375rem',
|
||||
py: '0.125rem',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '0.875em',
|
||||
fontFamily: 'monospace',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
border: '1px solid',
|
||||
borderColor: 'rgba(139, 92, 246, 0.3)',
|
||||
},
|
||||
'& pre': {
|
||||
bg: 'rgba(0, 0, 0, 0.5)',
|
||||
border: '1px solid',
|
||||
borderColor: 'rgba(139, 92, 246, 0.3)',
|
||||
color: 'rgba(229, 231, 235, 0.95)',
|
||||
p: '1rem',
|
||||
borderRadius: '0.5rem',
|
||||
overflow: 'auto',
|
||||
mb: '1.25rem',
|
||||
},
|
||||
'& pre code': {
|
||||
bg: 'transparent',
|
||||
p: '0',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||
pl: '1rem',
|
||||
py: '0.5rem',
|
||||
my: '1.5rem',
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
fontStyle: 'italic',
|
||||
bg: 'rgba(139, 92, 246, 0.05)',
|
||||
borderRadius: '0 0.25rem 0.25rem 0',
|
||||
},
|
||||
'& hr': {
|
||||
my: '2rem',
|
||||
borderColor: 'rgba(75, 85, 99, 0.5)',
|
||||
},
|
||||
'& table': {
|
||||
width: '100%',
|
||||
mb: '1.25rem',
|
||||
borderCollapse: 'collapse',
|
||||
},
|
||||
'& th': {
|
||||
bg: 'rgba(139, 92, 246, 0.2)',
|
||||
px: '1rem',
|
||||
py: '0.75rem',
|
||||
textAlign: 'left',
|
||||
fontWeight: '600',
|
||||
borderBottom: '2px solid',
|
||||
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
},
|
||||
'& td': {
|
||||
px: '1rem',
|
||||
py: '0.75rem',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'rgba(75, 85, 99, 0.3)',
|
||||
color: 'rgba(209, 213, 219, 0.9)',
|
||||
},
|
||||
'& tr:hover td': {
|
||||
bg: 'rgba(139, 92, 246, 0.05)',
|
||||
},
|
||||
})}
|
||||
dangerouslySetInnerHTML={{ __html: post.html }}
|
||||
/>
|
||||
</article>
|
||||
|
||||
{/* JSON-LD Structured Data */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: post.title,
|
||||
description: post.description,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: post.author,
|
||||
},
|
||||
datePublished: post.publishedAt,
|
||||
dateModified: post.updatedAt,
|
||||
keywords: post.tags.join(', '),
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { getAllPostsMetadata, getFeaturedPosts } from '@/lib/blog'
|
||||
import { css } from '../../../styled-system/css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog | Abaci.one',
|
||||
description:
|
||||
'Articles about educational technology, pedagogy, and innovative approaches to learning with the abacus.',
|
||||
openGraph: {
|
||||
title: 'Abaci.one Blog',
|
||||
description:
|
||||
'Articles about educational technology, pedagogy, and innovative approaches to learning with the abacus.',
|
||||
url: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://abaci.one'}/blog`,
|
||||
siteName: 'Abaci.one',
|
||||
type: 'website',
|
||||
},
|
||||
}
|
||||
|
||||
export default async function BlogIndex() {
|
||||
const featuredPosts = await getFeaturedPosts()
|
||||
const allPosts = await getAllPostsMetadata()
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="blog-index-page"
|
||||
className={css({
|
||||
minH: '100vh',
|
||||
bg: 'gray.900',
|
||||
pt: 'var(--app-nav-height-full)',
|
||||
})}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
opacity: 0.05,
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
|
||||
backgroundSize: '40px 40px',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
maxW: '64rem',
|
||||
mx: 'auto',
|
||||
px: { base: '1rem', md: '2rem' },
|
||||
py: { base: '2rem', md: '4rem' },
|
||||
})}
|
||||
>
|
||||
{/* Page Header */}
|
||||
<header
|
||||
data-section="page-header"
|
||||
className={css({
|
||||
mb: '3rem',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '2.5rem', md: '3.5rem' },
|
||||
fontWeight: 'bold',
|
||||
mb: '1rem',
|
||||
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
})}
|
||||
>
|
||||
Blog
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: '1.125rem', md: '1.25rem' },
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
maxW: '42rem',
|
||||
mx: 'auto',
|
||||
})}
|
||||
>
|
||||
Exploring educational technology, pedagogy, and innovative approaches to learning.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Featured Posts */}
|
||||
{featuredPosts.length > 0 && (
|
||||
<section
|
||||
data-section="featured-posts"
|
||||
className={css({
|
||||
mb: '4rem',
|
||||
})}
|
||||
>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '1.5rem', md: '1.875rem' },
|
||||
fontWeight: 'bold',
|
||||
mb: '1.5rem',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
})}
|
||||
>
|
||||
Featured
|
||||
</h2>
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr', md: 'repeat(auto-fit, minmax(300px, 1fr))' },
|
||||
gap: '1.5rem',
|
||||
})}
|
||||
>
|
||||
{featuredPosts.map((post) => {
|
||||
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/${post.slug}`}
|
||||
data-action="view-featured-post"
|
||||
className={css({
|
||||
display: 'block',
|
||||
p: '1.5rem',
|
||||
bg: 'rgba(139, 92, 246, 0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: '0.75rem',
|
||||
border: '1px solid',
|
||||
borderColor: 'rgba(139, 92, 246, 0.3)',
|
||||
transition: 'all 0.3s',
|
||||
_hover: {
|
||||
bg: 'rgba(139, 92, 246, 0.15)',
|
||||
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: '0 8px 24px rgba(139, 92, 246, 0.2)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: { base: '1.25rem', md: '1.5rem' },
|
||||
fontWeight: '600',
|
||||
mb: '0.5rem',
|
||||
color: 'white',
|
||||
})}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
<p
|
||||
className={css({
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
mb: '1rem',
|
||||
lineHeight: '1.6',
|
||||
})}
|
||||
>
|
||||
{post.excerpt || post.description}
|
||||
</p>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.75rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.875rem',
|
||||
color: 'rgba(196, 181, 253, 0.8)',
|
||||
})}
|
||||
>
|
||||
<span>{post.author}</span>
|
||||
<span>•</span>
|
||||
<time dateTime={post.publishedAt}>{publishedDate}</time>
|
||||
</div>
|
||||
{post.tags.length > 0 && (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
mt: '1rem',
|
||||
})}
|
||||
>
|
||||
{post.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={css({
|
||||
px: '0.5rem',
|
||||
py: '0.125rem',
|
||||
bg: 'rgba(139, 92, 246, 0.2)',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '500',
|
||||
})}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* All Posts */}
|
||||
<section data-section="all-posts">
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '1.5rem', md: '1.875rem' },
|
||||
fontWeight: 'bold',
|
||||
mb: '1.5rem',
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
})}
|
||||
>
|
||||
All Posts
|
||||
</h2>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2rem',
|
||||
})}
|
||||
>
|
||||
{allPosts.map((post) => {
|
||||
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
return (
|
||||
<article
|
||||
key={post.slug}
|
||||
data-element="post-preview"
|
||||
className={css({
|
||||
pb: '2rem',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'rgba(75, 85, 99, 0.5)',
|
||||
_last: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
data-action="view-post"
|
||||
className={css({
|
||||
display: 'block',
|
||||
_hover: {
|
||||
'& h3': {
|
||||
color: 'rgba(196, 181, 253, 1)',
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: { base: '1.5rem', md: '1.875rem' },
|
||||
fontWeight: '600',
|
||||
mb: '0.5rem',
|
||||
color: 'white',
|
||||
transition: 'color 0.2s',
|
||||
})}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.75rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.875rem',
|
||||
color: 'rgba(196, 181, 253, 0.7)',
|
||||
mb: '1rem',
|
||||
})}
|
||||
>
|
||||
<span>{post.author}</span>
|
||||
<span>•</span>
|
||||
<time dateTime={post.publishedAt}>{publishedDate}</time>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={css({
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
lineHeight: '1.6',
|
||||
mb: '1rem',
|
||||
})}
|
||||
>
|
||||
{post.excerpt || post.description}
|
||||
</p>
|
||||
|
||||
{post.tags.length > 0 && (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
{post.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={css({
|
||||
px: '0.5rem',
|
||||
py: '0.125rem',
|
||||
bg: 'rgba(75, 85, 99, 0.5)',
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '500',
|
||||
})}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { AbacusDisplayDropdown } from '@/components/AbacusDisplayDropdown'
|
||||
|
||||
interface CalendarConfigPanelProps {
|
||||
month: number
|
||||
year: number
|
||||
format: 'monthly' | 'daily'
|
||||
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
|
||||
isGenerating: boolean
|
||||
onMonthChange: (month: number) => void
|
||||
onYearChange: (year: number) => void
|
||||
onFormatChange: (format: 'monthly' | 'daily') => void
|
||||
onPaperSizeChange: (size: 'us-letter' | 'a4' | 'a3' | 'tabloid') => void
|
||||
onGenerate: () => void
|
||||
}
|
||||
|
||||
export function CalendarConfigPanel({
|
||||
month,
|
||||
year,
|
||||
format,
|
||||
paperSize,
|
||||
isGenerating,
|
||||
onMonthChange,
|
||||
onYearChange,
|
||||
onFormatChange,
|
||||
onPaperSizeChange,
|
||||
onGenerate,
|
||||
}: CalendarConfigPanelProps) {
|
||||
const t = useTranslations('calendar')
|
||||
const abacusConfig = useAbacusConfig()
|
||||
|
||||
const MONTHS = [
|
||||
t('months.january'),
|
||||
t('months.february'),
|
||||
t('months.march'),
|
||||
t('months.april'),
|
||||
t('months.may'),
|
||||
t('months.june'),
|
||||
t('months.july'),
|
||||
t('months.august'),
|
||||
t('months.september'),
|
||||
t('months.october'),
|
||||
t('months.november'),
|
||||
t('months.december'),
|
||||
]
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="calendar-config-panel"
|
||||
className={css({
|
||||
bg: 'gray.800',
|
||||
borderRadius: '12px',
|
||||
padding: '1.5rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1.5rem',
|
||||
})}
|
||||
>
|
||||
{/* Format Selection */}
|
||||
<fieldset
|
||||
data-section="format-selection"
|
||||
className={css({
|
||||
border: 'none',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
})}
|
||||
>
|
||||
<legend
|
||||
className={css({
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
marginBottom: '0.75rem',
|
||||
color: 'yellow.400',
|
||||
})}
|
||||
>
|
||||
{t('format.title')}
|
||||
</legend>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<label
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '6px',
|
||||
_hover: { bg: 'gray.700' },
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value="monthly"
|
||||
checked={format === 'monthly'}
|
||||
onChange={(e) => onFormatChange(e.target.value as 'monthly' | 'daily')}
|
||||
className={css({
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
<span>{t('format.monthly')}</span>
|
||||
</label>
|
||||
<label
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '6px',
|
||||
_hover: { bg: 'gray.700' },
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value="daily"
|
||||
checked={format === 'daily'}
|
||||
onChange={(e) => onFormatChange(e.target.value as 'monthly' | 'daily')}
|
||||
className={css({
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
<span>{t('format.daily')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Date Selection */}
|
||||
<fieldset
|
||||
data-section="date-selection"
|
||||
className={css({
|
||||
border: 'none',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
})}
|
||||
>
|
||||
<legend
|
||||
className={css({
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
marginBottom: '0.75rem',
|
||||
color: 'yellow.400',
|
||||
})}
|
||||
>
|
||||
{t('date.title')}
|
||||
</legend>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<select
|
||||
data-element="month-select"
|
||||
value={month}
|
||||
onChange={(e) => onMonthChange(Number(e.target.value))}
|
||||
className={css({
|
||||
flex: '1',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '6px',
|
||||
bg: 'gray.700',
|
||||
color: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.600',
|
||||
cursor: 'pointer',
|
||||
_hover: { borderColor: 'gray.500' },
|
||||
})}
|
||||
>
|
||||
{MONTHS.map((monthName, index) => (
|
||||
<option key={monthName} value={index + 1}>
|
||||
{monthName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
data-element="year-input"
|
||||
value={year}
|
||||
onChange={(e) => onYearChange(Number(e.target.value))}
|
||||
min={1}
|
||||
max={9999}
|
||||
className={css({
|
||||
width: '100px',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '6px',
|
||||
bg: 'gray.700',
|
||||
color: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.600',
|
||||
_hover: { borderColor: 'gray.500' },
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Paper Size */}
|
||||
<fieldset
|
||||
data-section="paper-size"
|
||||
className={css({
|
||||
border: 'none',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
})}
|
||||
>
|
||||
<legend
|
||||
className={css({
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
marginBottom: '0.75rem',
|
||||
color: 'yellow.400',
|
||||
})}
|
||||
>
|
||||
{t('paperSize.title')}
|
||||
</legend>
|
||||
<select
|
||||
data-element="paper-size-select"
|
||||
value={paperSize}
|
||||
onChange={(e) =>
|
||||
onPaperSizeChange(e.target.value as 'us-letter' | 'a4' | 'a3' | 'tabloid')
|
||||
}
|
||||
className={css({
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '6px',
|
||||
bg: 'gray.700',
|
||||
color: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.600',
|
||||
cursor: 'pointer',
|
||||
_hover: { borderColor: 'gray.500' },
|
||||
})}
|
||||
>
|
||||
<option value="us-letter">{t('paperSize.usLetter')}</option>
|
||||
<option value="a4">{t('paperSize.a4')}</option>
|
||||
<option value="a3">{t('paperSize.a3')}</option>
|
||||
<option value="tabloid">{t('paperSize.tabloid')}</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
{/* Abacus Styling */}
|
||||
<div
|
||||
data-section="styling-info"
|
||||
className={css({
|
||||
padding: '1rem',
|
||||
bg: 'gray.700',
|
||||
borderRadius: '8px',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: '0.875rem',
|
||||
marginBottom: '0.75rem',
|
||||
color: 'gray.300',
|
||||
})}
|
||||
>
|
||||
{t('styling.preview')}
|
||||
</p>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '0.75rem',
|
||||
})}
|
||||
>
|
||||
<AbacusReact
|
||||
value={12}
|
||||
columns={2}
|
||||
customStyles={abacusConfig.customStyles}
|
||||
scaleFactor={0.5}
|
||||
showNumbers={false}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
<AbacusDisplayDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
type="button"
|
||||
data-action="generate-calendar"
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating}
|
||||
className={css({
|
||||
padding: '1rem',
|
||||
bg: 'yellow.500',
|
||||
color: 'gray.900',
|
||||
fontWeight: '600',
|
||||
fontSize: '1.125rem',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: 'yellow.400',
|
||||
},
|
||||
_disabled: {
|
||||
bg: 'gray.600',
|
||||
color: 'gray.400',
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{isGenerating ? t('generate.generating') : t('generate.button')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
|
||||
interface CalendarPreviewProps {
|
||||
month: number
|
||||
year: number
|
||||
format: 'monthly' | 'daily'
|
||||
previewSvg: string | null
|
||||
}
|
||||
|
||||
async function fetchTypstPreview(
|
||||
month: number,
|
||||
year: number,
|
||||
format: string
|
||||
): Promise<string | null> {
|
||||
const response = await fetch('/api/create/calendar/preview', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ month, year, format }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || errorData.message || 'Failed to fetch preview')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.svg
|
||||
}
|
||||
|
||||
export function CalendarPreview({ month, year, format, previewSvg }: CalendarPreviewProps) {
|
||||
const t = useTranslations('calendar')
|
||||
// Use React Query to fetch Typst-generated preview (client-side only)
|
||||
const { data: typstPreviewSvg, isLoading } = useQuery({
|
||||
queryKey: ['calendar-typst-preview', month, year, format],
|
||||
queryFn: () => fetchTypstPreview(month, year, format),
|
||||
enabled: typeof window !== 'undefined', // Run on client for both formats
|
||||
})
|
||||
|
||||
// Use generated PDF SVG if available, otherwise use Typst live preview
|
||||
const displaySvg = previewSvg || typstPreviewSvg
|
||||
|
||||
// Show loading state while fetching preview
|
||||
if (isLoading || !displaySvg) {
|
||||
return (
|
||||
<div
|
||||
data-component="calendar-preview"
|
||||
className={css({
|
||||
bg: 'gray.800',
|
||||
borderRadius: '12px',
|
||||
padding: '2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '600px',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: '1.25rem',
|
||||
color: 'gray.400',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{isLoading ? t('preview.loading') : t('preview.noPreview')}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="calendar-preview"
|
||||
className={css({
|
||||
bg: 'gray.800',
|
||||
borderRadius: '12px',
|
||||
padding: '2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: '1.125rem',
|
||||
color: 'yellow.400',
|
||||
marginBottom: '1rem',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{previewSvg
|
||||
? t('preview.generatedPdf')
|
||||
: format === 'daily'
|
||||
? t('preview.livePreviewFirstDay')
|
||||
: t('preview.livePreview')}
|
||||
</p>
|
||||
<div
|
||||
className={css({
|
||||
bg: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '1rem',
|
||||
maxWidth: '100%',
|
||||
overflow: 'auto',
|
||||
})}
|
||||
dangerouslySetInnerHTML={{ __html: displaySvg }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { CalendarConfigPanel } from './components/CalendarConfigPanel'
|
||||
import { CalendarPreview } from './components/CalendarPreview'
|
||||
|
||||
export default function CalendarCreatorPage() {
|
||||
const t = useTranslations('calendar')
|
||||
const currentDate = new Date()
|
||||
const abacusConfig = useAbacusConfig()
|
||||
const [month, setMonth] = useState(currentDate.getMonth() + 1) // 1-12
|
||||
const [year, setYear] = useState(currentDate.getFullYear())
|
||||
const [format, setFormat] = useState<'monthly' | 'daily'>('monthly')
|
||||
const [paperSize, setPaperSize] = useState<'us-letter' | 'a4' | 'a3' | 'tabloid'>('us-letter')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [previewSvg, setPreviewSvg] = useState<string | null>(null)
|
||||
|
||||
// Detect default paper size based on user's locale (client-side only)
|
||||
useEffect(() => {
|
||||
// Get user's locale
|
||||
const locale = navigator.language || navigator.languages?.[0] || 'en-US'
|
||||
const country = locale.split('-')[1]?.toUpperCase()
|
||||
|
||||
// Countries that use US Letter (8.5" × 11")
|
||||
const letterCountries = ['US', 'CA', 'MX', 'GT', 'PA', 'DO', 'PR', 'PH']
|
||||
|
||||
const detectedSize = letterCountries.includes(country || '') ? 'us-letter' : 'a4'
|
||||
setPaperSize(detectedSize)
|
||||
}, [])
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setIsGenerating(true)
|
||||
try {
|
||||
const response = await fetch('/api/create/calendar/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
month,
|
||||
year,
|
||||
format,
|
||||
paperSize,
|
||||
abacusConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.message || 'Failed to generate calendar')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Convert base64 PDF to blob and trigger download
|
||||
const pdfBytes = Uint8Array.from(atob(data.pdf), (c) => c.charCodeAt(0))
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = data.filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} catch (error) {
|
||||
console.error('Error generating calendar:', error)
|
||||
alert(
|
||||
`Failed to generate calendar: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle="Create" navEmoji="📅">
|
||||
<div
|
||||
data-component="calendar-creator"
|
||||
className={`with-fixed-nav ${css({
|
||||
minHeight: '100vh',
|
||||
bg: 'gray.900',
|
||||
color: 'white',
|
||||
padding: '2rem',
|
||||
})}`}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
maxWidth: '1400px',
|
||||
margin: '0 auto',
|
||||
})}
|
||||
>
|
||||
{/* Header */}
|
||||
<header
|
||||
data-section="page-header"
|
||||
className={css({
|
||||
textAlign: 'center',
|
||||
marginBottom: '3rem',
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: '2.5rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
color: 'yellow.400',
|
||||
})}
|
||||
>
|
||||
{t('pageTitle')}
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: '1.125rem',
|
||||
color: 'gray.300',
|
||||
})}
|
||||
>
|
||||
{t('pageSubtitle')}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr', lg: '350px 1fr' },
|
||||
gap: '2rem',
|
||||
})}
|
||||
>
|
||||
{/* Configuration Panel */}
|
||||
<CalendarConfigPanel
|
||||
month={month}
|
||||
year={year}
|
||||
format={format}
|
||||
paperSize={paperSize}
|
||||
isGenerating={isGenerating}
|
||||
onMonthChange={setMonth}
|
||||
onYearChange={setYear}
|
||||
onFormatChange={setFormat}
|
||||
onPaperSizeChange={setPaperSize}
|
||||
onGenerate={handleGenerate}
|
||||
/>
|
||||
|
||||
{/* Preview */}
|
||||
<CalendarPreview month={month} year={year} format={format} previewSvg={previewSvg} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState } from 'react'
|
||||
import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationFormWithoutGenerate'
|
||||
import { GenerationProgress } from '@/components/GenerationProgress'
|
||||
import { FlashcardPreview } from '@/components/FlashcardPreview'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { StyleControls } from '@/components/StyleControls'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { container, grid, hstack, stack } from '../../../../styled-system/patterns'
|
||||
|
||||
// Complete, validated configuration ready for generation
|
||||
export interface FlashcardConfig {
|
||||
range: string
|
||||
step?: number
|
||||
cardsPerPage?: number
|
||||
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
|
||||
orientation?: 'portrait' | 'landscape'
|
||||
margins?: {
|
||||
top?: string
|
||||
bottom?: string
|
||||
left?: string
|
||||
right?: string
|
||||
}
|
||||
gutter?: string
|
||||
shuffle?: boolean
|
||||
seed?: number
|
||||
showCutMarks?: boolean
|
||||
showRegistration?: boolean
|
||||
fontFamily?: string
|
||||
fontSize?: string
|
||||
columns?: string | number
|
||||
showEmptyColumns?: boolean
|
||||
hideInactiveBeads?: boolean
|
||||
beadShape?: 'diamond' | 'circle' | 'square'
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
|
||||
coloredNumerals?: boolean
|
||||
scaleFactor?: number
|
||||
format?: 'pdf' | 'html' | 'png' | 'svg'
|
||||
}
|
||||
|
||||
// Partial form state during editing (may have undefined values)
|
||||
export interface FlashcardFormState {
|
||||
range?: string
|
||||
step?: number
|
||||
cardsPerPage?: number
|
||||
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
|
||||
orientation?: 'portrait' | 'landscape'
|
||||
margins?: {
|
||||
top?: string
|
||||
bottom?: string
|
||||
left?: string
|
||||
right?: string
|
||||
}
|
||||
gutter?: string
|
||||
shuffle?: boolean
|
||||
seed?: number
|
||||
showCutMarks?: boolean
|
||||
showRegistration?: boolean
|
||||
fontFamily?: string
|
||||
fontSize?: string
|
||||
columns?: string | number
|
||||
showEmptyColumns?: boolean
|
||||
hideInactiveBeads?: boolean
|
||||
beadShape?: 'diamond' | 'circle' | 'square'
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
|
||||
coloredNumerals?: boolean
|
||||
scaleFactor?: number
|
||||
format?: 'pdf' | 'html' | 'png' | 'svg'
|
||||
}
|
||||
|
||||
// Validation function to convert form state to complete config
|
||||
function validateAndCompleteConfig(formState: FlashcardFormState): FlashcardConfig {
|
||||
return {
|
||||
// Required fields with defaults
|
||||
range: formState.range || '0-99',
|
||||
|
||||
// Optional fields with defaults
|
||||
step: formState.step ?? 1,
|
||||
cardsPerPage: formState.cardsPerPage ?? 6,
|
||||
paperSize: formState.paperSize ?? 'us-letter',
|
||||
orientation: formState.orientation ?? 'portrait',
|
||||
gutter: formState.gutter ?? '5mm',
|
||||
shuffle: formState.shuffle ?? false,
|
||||
seed: formState.seed,
|
||||
showCutMarks: formState.showCutMarks ?? false,
|
||||
showRegistration: formState.showRegistration ?? false,
|
||||
fontFamily: formState.fontFamily ?? 'DejaVu Sans',
|
||||
fontSize: formState.fontSize ?? '48pt',
|
||||
columns: formState.columns ?? 'auto',
|
||||
showEmptyColumns: formState.showEmptyColumns ?? false,
|
||||
hideInactiveBeads: formState.hideInactiveBeads ?? false,
|
||||
beadShape: formState.beadShape ?? 'diamond',
|
||||
colorScheme: formState.colorScheme ?? 'place-value',
|
||||
coloredNumerals: formState.coloredNumerals ?? false,
|
||||
scaleFactor: formState.scaleFactor ?? 0.9,
|
||||
format: formState.format ?? 'pdf',
|
||||
margins: formState.margins,
|
||||
}
|
||||
}
|
||||
|
||||
type GenerationStatus = 'idle' | 'generating' | 'error'
|
||||
|
||||
export default function CreatePage() {
|
||||
const t = useTranslations('create.flashcards')
|
||||
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const globalConfig = useAbacusConfig()
|
||||
|
||||
const form = useForm<FlashcardFormState>({
|
||||
defaultValues: {
|
||||
range: '0-99',
|
||||
step: 1,
|
||||
cardsPerPage: 6,
|
||||
paperSize: 'us-letter',
|
||||
orientation: 'portrait',
|
||||
gutter: '5mm',
|
||||
shuffle: false,
|
||||
showCutMarks: false,
|
||||
showRegistration: false,
|
||||
fontFamily: 'DejaVu Sans',
|
||||
fontSize: '48pt',
|
||||
columns: 'auto',
|
||||
showEmptyColumns: false,
|
||||
// Use global config for abacus display settings
|
||||
hideInactiveBeads: globalConfig.hideInactiveBeads,
|
||||
beadShape: globalConfig.beadShape,
|
||||
colorScheme: globalConfig.colorScheme,
|
||||
coloredNumerals: globalConfig.coloredNumerals,
|
||||
scaleFactor: globalConfig.scaleFactor,
|
||||
format: 'pdf',
|
||||
},
|
||||
})
|
||||
|
||||
const handleGenerate = async (formState: FlashcardFormState) => {
|
||||
setGenerationStatus('generating')
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Validate and complete the configuration
|
||||
const config = validateAndCompleteConfig(formState)
|
||||
|
||||
const response = await fetch('/api/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Handle error response (should be JSON)
|
||||
const errorResult = await response.json()
|
||||
throw new Error(errorResult.error || 'Generation failed')
|
||||
}
|
||||
|
||||
// Success - response is binary PDF data, trigger download
|
||||
const blob = await response.blob()
|
||||
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
|
||||
|
||||
// Create download link and trigger download
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
setGenerationStatus('idle') // Reset to idle after successful download
|
||||
} catch (err) {
|
||||
console.error('Generation error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
setGenerationStatus('error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewGeneration = () => {
|
||||
setGenerationStatus('idle')
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle={t('navTitle')} navEmoji="✨">
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
{/* Main Content */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
|
||||
<div className={stack({ gap: '6', mb: '8' })}>
|
||||
<div className={stack({ gap: '2', textAlign: 'center' })}>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('pageTitle')}
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
color: 'gray.600',
|
||||
})}
|
||||
>
|
||||
{t('pageSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Interface */}
|
||||
<div
|
||||
className={grid({
|
||||
columns: { base: 1, lg: 3 },
|
||||
gap: '8',
|
||||
alignItems: 'start',
|
||||
})}
|
||||
>
|
||||
{/* Main Configuration Panel */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
p: '8',
|
||||
})}
|
||||
>
|
||||
<ConfigurationFormWithoutGenerate form={form} />
|
||||
</div>
|
||||
|
||||
{/* Style Controls Panel */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
p: '6',
|
||||
})}
|
||||
>
|
||||
<div className={stack({ gap: '4' })}>
|
||||
<div className={stack({ gap: '1' })}>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('stylePanel.title')}
|
||||
</h3>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.600',
|
||||
})}
|
||||
>
|
||||
{t('stylePanel.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form.Subscribe
|
||||
selector={(state) => state}
|
||||
children={(_state) => <StyleControls form={form} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Preview Panel */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
p: '6',
|
||||
})}
|
||||
>
|
||||
<div className={stack({ gap: '6' })}>
|
||||
<form.Subscribe
|
||||
selector={(state) => state}
|
||||
children={(state) => <FlashcardPreview config={state.values} />}
|
||||
/>
|
||||
|
||||
{/* Generate Button within Preview */}
|
||||
<div
|
||||
className={css({
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
pt: '6',
|
||||
})}
|
||||
>
|
||||
{/* Generation Status */}
|
||||
{generationStatus === 'generating' && (
|
||||
<div className={css({ mb: '4' })}>
|
||||
<GenerationProgress config={form.state.values} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleGenerate(form.state.values)}
|
||||
disabled={generationStatus === 'generating'}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: '6',
|
||||
py: '4',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
rounded: 'xl',
|
||||
shadow: 'card',
|
||||
transition: 'all',
|
||||
cursor: generationStatus === 'generating' ? 'not-allowed' : 'pointer',
|
||||
opacity: generationStatus === 'generating' ? '0.7' : '1',
|
||||
_hover:
|
||||
generationStatus === 'generating'
|
||||
? {}
|
||||
: {
|
||||
bg: 'brand.700',
|
||||
transform: 'translateY(-1px)',
|
||||
shadow: 'modal',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={hstack({ gap: '3', justify: 'center' })}>
|
||||
{generationStatus === 'generating' ? (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
w: '5',
|
||||
h: '5',
|
||||
border: '2px solid',
|
||||
borderColor: 'white',
|
||||
borderTopColor: 'transparent',
|
||||
rounded: 'full',
|
||||
animation: 'spin 1s linear infinite',
|
||||
})}
|
||||
/>
|
||||
{t('generate.generating')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={css({ fontSize: 'xl' })}>✨</div>
|
||||
{t('generate.button')}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display - moved to global level */}
|
||||
{generationStatus === 'error' && error && (
|
||||
<div
|
||||
className={css({
|
||||
bg: 'red.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'red.200',
|
||||
rounded: '2xl',
|
||||
p: '8',
|
||||
mt: '8',
|
||||
})}
|
||||
>
|
||||
<div className={stack({ gap: '4' })}>
|
||||
<div className={hstack({ gap: '3', alignItems: 'center' })}>
|
||||
<div className={css({ fontSize: '2xl' })}>❌</div>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'semibold',
|
||||
color: 'red.800',
|
||||
})}
|
||||
>
|
||||
{t('error.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<p
|
||||
className={css({
|
||||
color: 'red.700',
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleNewGeneration}
|
||||
className={css({
|
||||
alignSelf: 'start',
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'red.600',
|
||||
color: 'white',
|
||||
fontWeight: 'medium',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'red.700' },
|
||||
})}
|
||||
>
|
||||
{t('error.tryAgain')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
# Worksheet Config Schema Guide
|
||||
|
||||
## Type-Safe JSON Blob with Schema Versioning
|
||||
|
||||
This system provides type-safe storage of worksheet settings using JSON blobs with automatic schema migration.
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Runtime Type Safety**: Zod validates all configs at runtime
|
||||
2. **Schema Versioning**: Each config has a `version` field for evolution
|
||||
3. **Automatic Migration**: Old configs automatically upgrade to latest version
|
||||
4. **Graceful Degradation**: Invalid configs fall back to sensible defaults
|
||||
5. **Future-Proof**: Add new worksheet types without schema changes
|
||||
|
||||
## Adding a New Setting to Existing Worksheet Type
|
||||
|
||||
**Example**: Add `showHints` to addition worksheets
|
||||
|
||||
1. **Update the schema** (`config-schemas.ts`):
|
||||
```typescript
|
||||
export const additionConfigV2Schema = z.object({
|
||||
version: z.literal(2),
|
||||
// ... all V1 fields ...
|
||||
showHints: z.boolean(), // NEW FIELD
|
||||
})
|
||||
```
|
||||
|
||||
2. **Create migration function**:
|
||||
```typescript
|
||||
function migrateAdditionV1toV2(v1: AdditionConfigV1): AdditionConfigV2 {
|
||||
return {
|
||||
...v1,
|
||||
version: 2,
|
||||
showHints: false, // Default value for new field
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update discriminated union**:
|
||||
```typescript
|
||||
export const additionConfigSchema = z.discriminatedUnion('version', [
|
||||
additionConfigV1Schema,
|
||||
additionConfigV2Schema, // Add new version
|
||||
])
|
||||
```
|
||||
|
||||
4. **Update migration switch**:
|
||||
```typescript
|
||||
export function migrateAdditionConfig(rawConfig: unknown): AdditionConfigV2 {
|
||||
const parsed = additionConfigSchema.safeParse(rawConfig)
|
||||
if (!parsed.success) return defaultAdditionConfig
|
||||
|
||||
switch (parsed.data.version) {
|
||||
case 1:
|
||||
return migrateAdditionV1toV2(parsed.data)
|
||||
case 2:
|
||||
return parsed.data // Latest version
|
||||
default:
|
||||
return defaultAdditionConfig
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Update default**:
|
||||
```typescript
|
||||
export const defaultAdditionConfig: AdditionConfigV2 = {
|
||||
version: 2,
|
||||
// ... all fields including showHints: false
|
||||
}
|
||||
```
|
||||
|
||||
## Adding a New Worksheet Type
|
||||
|
||||
**Example**: Add multiplication worksheets
|
||||
|
||||
1. **Create schema**:
|
||||
```typescript
|
||||
export const multiplicationConfigV1Schema = z.object({
|
||||
version: z.literal(1),
|
||||
problemsPerPage: z.number().int().min(1).max(100),
|
||||
showTimes Table: z.boolean(),
|
||||
// ... multiplication-specific fields
|
||||
})
|
||||
```
|
||||
|
||||
2. **Create helpers**:
|
||||
```typescript
|
||||
export function parseMultiplicationConfig(jsonString: string): MultiplicationConfigV1 {
|
||||
try {
|
||||
const raw = JSON.parse(jsonString)
|
||||
return multiplicationConfigSchema.parse(raw)
|
||||
} catch (error) {
|
||||
return defaultMultiplicationConfig
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeMultiplicationConfig(config: Omit<MultiplicationConfigV1, 'version'>): string {
|
||||
return JSON.stringify({ ...config, version: 1 })
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use in app**: No database migration needed!
|
||||
|
||||
## Usage in Application Code
|
||||
|
||||
### Saving Settings
|
||||
|
||||
```typescript
|
||||
import { serializeAdditionConfig } from './config-schemas'
|
||||
|
||||
const configJSON = serializeAdditionConfig({
|
||||
problemsPerPage: 20,
|
||||
cols: 5,
|
||||
// ... all other fields (version added automatically)
|
||||
})
|
||||
|
||||
await db.insert(worksheetSettings).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: user.id,
|
||||
worksheetType: 'addition',
|
||||
config: configJSON,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
```
|
||||
|
||||
### Loading Settings
|
||||
|
||||
```typescript
|
||||
import { parseAdditionConfig } from './config-schemas'
|
||||
|
||||
const row = await db
|
||||
.select()
|
||||
.from(worksheetSettings)
|
||||
.where(eq(worksheetSettings.userId, userId))
|
||||
.where(eq(worksheetSettings.worksheetType, 'addition'))
|
||||
.limit(1)
|
||||
|
||||
const config = row ? parseAdditionConfig(row.config) : defaultAdditionConfig
|
||||
// config is now type-safe and guaranteed to be latest version!
|
||||
```
|
||||
|
||||
## Migration Examples
|
||||
|
||||
### Scenario 1: User has V1 config, app is on V2
|
||||
|
||||
```json
|
||||
// Stored in DB (V1):
|
||||
{
|
||||
"version": 1,
|
||||
"problemsPerPage": 20,
|
||||
"cols": 5,
|
||||
...
|
||||
}
|
||||
|
||||
// Automatically migrated to V2 when loaded:
|
||||
{
|
||||
"version": 2,
|
||||
"problemsPerPage": 20,
|
||||
"cols": 5,
|
||||
"showHints": false, // <-- Added with default
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 2: Invalid/corrupted config
|
||||
|
||||
```typescript
|
||||
// Stored in DB (corrupted):
|
||||
"{invalid json{{"
|
||||
|
||||
// Falls back to defaults:
|
||||
parseAdditionConfig("{invalid json{{")
|
||||
// Returns: defaultAdditionConfig
|
||||
```
|
||||
|
||||
### Scenario 3: Future version (app downgrade)
|
||||
|
||||
```json
|
||||
// Stored in DB (V3, unknown to current app):
|
||||
{
|
||||
"version": 3,
|
||||
"someNewField": "value",
|
||||
...
|
||||
}
|
||||
|
||||
// Falls back to defaults (can't parse unknown version):
|
||||
// Returns: defaultAdditionConfig
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Never remove fields in new versions** - only add (backwards compatible)
|
||||
2. **Always provide defaults** in migration functions
|
||||
3. **Test migrations** with real V1 data before deploying V2
|
||||
4. **Document breaking changes** if absolutely necessary
|
||||
5. **Keep CURRENT_VERSION constant** in sync with latest schema
|
||||
|
||||
## Type Safety Benefits
|
||||
|
||||
```typescript
|
||||
// TypeScript catches missing fields at compile time:
|
||||
const config: AdditionConfigV1 = {
|
||||
version: 1,
|
||||
problemsPerPage: 20,
|
||||
// ❌ Error: Missing required field 'cols'
|
||||
}
|
||||
|
||||
// Runtime validation catches invalid values:
|
||||
parseAdditionConfig('{"version": 1, "cols": 999}')
|
||||
// ❌ Zod error: cols must be <= 10, falls back to defaults
|
||||
|
||||
// Full autocomplete in editors:
|
||||
config.show // ← autocomplete suggests: showCarryBoxes, showAnswerBoxes, etc.
|
||||
```
|
||||
|
||||
## Database Schema Evolution
|
||||
|
||||
**Important**: The database schema NEVER needs to change when:
|
||||
- Adding new worksheet types (just store different JSON)
|
||||
- Adding new fields to existing types (handled by migration)
|
||||
- Changing default values (handled in defaultConfig)
|
||||
|
||||
**Only needs migration when**:
|
||||
- Adding indexes for performance
|
||||
- Changing primary key structure
|
||||
- Adding completely new columns to worksheet_settings table itself
|
||||
@@ -1,507 +0,0 @@
|
||||
# Publication Plan: Constrained 2D Pedagogical Difficulty System
|
||||
|
||||
**Status**: Planning Stage
|
||||
**Created**: November 2025
|
||||
**Last Updated**: November 2025
|
||||
|
||||
## Related Files
|
||||
|
||||
- **Implementation**: [difficultyProfiles.ts](./difficultyProfiles.ts) - Core constraint system
|
||||
- **UI**: [ConfigPanel.tsx](./components/ConfigPanel.tsx) - Split button interface + debug graph
|
||||
- **Specification**: [SMART_DIFFICULTY_SPEC.md](./SMART_DIFFICULTY_SPEC.md) - Complete technical spec
|
||||
- **Verification**: [scripts/traceDifficultyPath.ts](../../../../../scripts/traceDifficultyPath.ts) - Path visualization
|
||||
- **Live Demo**: https://abaci.one/create/worksheets/addition
|
||||
|
||||
## The Innovation
|
||||
|
||||
### What We Built
|
||||
|
||||
A **constrained 2D pedagogical space** for addition worksheet difficulty that treats difficulty as two independent-but-constrained dimensions:
|
||||
|
||||
1. **Challenge Axis** (Regrouping): Problem complexity (0-18 levels)
|
||||
2. **Support Axis** (Scaffolding): Visual aids and guidance (0-12 levels)
|
||||
3. **Constraint Band**: Diagonal zone of valid (Challenge, Support) pairs
|
||||
|
||||
**Key Insight**: Higher challenge requires lower support (and vice versa), encoding pedagogical principles directly into the difficulty space.
|
||||
|
||||
### Why This Matters
|
||||
|
||||
**Problem with traditional 1D difficulty**:
|
||||
|
||||
- Conflates problem complexity with instructional support
|
||||
- Can't differentiate "ready for harder problems but still needs visual aids" from "struggling with current level"
|
||||
- Forces teachers into one-size-fits-all progression
|
||||
|
||||
**Our 2D approach enables**:
|
||||
|
||||
- Dimension-specific adjustments (challenge-only, support-only, or both)
|
||||
- Pedagogically-valid combinations only (no "hard problems + max scaffolding" or "easy problems + zero support")
|
||||
- Precise differentiation for individual student needs
|
||||
|
||||
### Theoretical Foundation
|
||||
|
||||
- **Zone of Proximal Development** (Vygotsky): Constraint band represents learnable space
|
||||
- **Cognitive Load Theory** (Sweller): Balance intrinsic load (challenge) vs extraneous load (from poor scaffolding)
|
||||
- **Scaffolding Fading** (Wood, Bruner, Ross): Support should decrease as mastery develops
|
||||
|
||||
## Publication Venues (Ranked by Fit)
|
||||
|
||||
### 1. ACM Learning @ Scale (L@S) - **BEST FIT**
|
||||
|
||||
**Why**: Novel systems for scalable personalized learning
|
||||
**Format**:
|
||||
|
||||
- Full paper: 10 pages
|
||||
- Work-in-Progress: 4 pages (easier entry point)
|
||||
|
||||
**Timeline**:
|
||||
|
||||
- Conference: June annually
|
||||
- Submission: ~January
|
||||
- Reviews: March
|
||||
- Camera-ready: April
|
||||
|
||||
**What they want**:
|
||||
|
||||
- Novel educational technology systems
|
||||
- Learning theory grounding
|
||||
- Evidence of impact (can be preliminary for WiP)
|
||||
- Scalability considerations
|
||||
|
||||
**URL**: https://learningatscale.acm.org/
|
||||
|
||||
**Strategy**: Submit WiP paper January 2026, full paper 2027 (with evaluation data)
|
||||
|
||||
### 2. International Journal of Artificial Intelligence in Education (IJAIED)
|
||||
|
||||
**Why**: AI-driven adaptive learning systems
|
||||
**Format**: Full article (25-40 pages typical)
|
||||
**Timeline**: Rolling submissions, 3-6 month review
|
||||
**URL**: https://link.springer.com/journal/40593
|
||||
|
||||
**What they want**:
|
||||
|
||||
- Computational/algorithmic contributions
|
||||
- Strong theoretical framework
|
||||
- Empirical validation required
|
||||
|
||||
**Strategy**: Target after teacher study (2026-2027)
|
||||
|
||||
### 3. Learning Analytics & Knowledge (LAK) Conference
|
||||
|
||||
**Why**: Data-driven educational design
|
||||
**Format**: Full paper (8-10 pages) or short (4 pages)
|
||||
**Timeline**: Annual (March), submission ~October
|
||||
**URL**: https://www.solaresearch.org/events/lak/
|
||||
|
||||
**What they want**:
|
||||
|
||||
- Use of learning analytics in design
|
||||
- Evidence from usage data
|
||||
- Insights from student/teacher behavior
|
||||
|
||||
**Strategy**: After collecting usage logs and learning outcome data
|
||||
|
||||
### 4. Journal of Educational Technology & Society (ETS)
|
||||
|
||||
**Why**: Educational technology innovations
|
||||
**Format**: ~20 pages, open access
|
||||
**Timeline**: Rolling submissions
|
||||
**URL**: https://www.j-ets.net/
|
||||
|
||||
**Strategy**: Backup venue if conference submissions don't work
|
||||
|
||||
## What We Have vs. What We Need
|
||||
|
||||
### ✅ Already Have
|
||||
|
||||
1. **Working Implementation**
|
||||
- Core constraint system ([difficultyProfiles.ts](./difficultyProfiles.ts))
|
||||
- Teacher-facing UI with split buttons ([ConfigPanel.tsx](./components/ConfigPanel.tsx))
|
||||
- Debug tools (clickable graph, trace script)
|
||||
- Live at https://abaci.one/create/worksheets/addition
|
||||
|
||||
2. **Technical Documentation**
|
||||
- Complete specification ([SMART_DIFFICULTY_SPEC.md](./SMART_DIFFICULTY_SPEC.md))
|
||||
- Algorithm descriptions
|
||||
- Architecture rationale
|
||||
|
||||
3. **Theoretical Framework**
|
||||
- ZPD mapping
|
||||
- Cognitive load theory connections
|
||||
- Scaffolding fading principles
|
||||
|
||||
### ⏳ Need for Publications
|
||||
|
||||
#### For Work-in-Progress Paper (4 pages, Jan 2026):
|
||||
|
||||
1. **Design Rationale** (1-2 pages)
|
||||
- Why 2D vs 1D?
|
||||
- How did we derive the constraint band?
|
||||
- What design alternatives did we consider?
|
||||
|
||||
2. **Related Work** (1 page)
|
||||
- Intelligent Tutoring Systems (ALEKS, ASSISTments, etc.)
|
||||
- Khan Academy's mastery learning
|
||||
- Adaptive difficulty systems
|
||||
- How is our approach different/better?
|
||||
|
||||
3. **Usage Scenarios** (0.5 pages)
|
||||
- Example teacher workflows
|
||||
- Screenshots showing the interface
|
||||
- How teachers would use dimension-specific adjustments
|
||||
|
||||
4. **Preliminary Evaluation** (0.5 pages)
|
||||
- Your own testing
|
||||
- Initial teacher feedback (if we can get some)
|
||||
- Identified limitations
|
||||
|
||||
#### For Full Research Paper (10 pages, 2027):
|
||||
|
||||
1. **Teacher Study** (Required)
|
||||
- 10-15 teachers using the system
|
||||
- Interview data: How did they use it? Was 2D helpful?
|
||||
- Usage logs: Which modes did they use? Navigation patterns?
|
||||
- Comparison group: Teachers using 1D slider version
|
||||
|
||||
2. **Student Learning Outcomes** (Ideal)
|
||||
- 40-60 students
|
||||
- Pre/post assessments
|
||||
- Compare: 2D constrained vs 1D slider vs fixed difficulty
|
||||
- Track learning trajectories over 6-8 weeks
|
||||
|
||||
3. **Quantitative Analysis**
|
||||
- Statistical significance of learning gains
|
||||
- Teacher satisfaction surveys
|
||||
- Student engagement metrics
|
||||
|
||||
## Publication Paths (3 Options)
|
||||
|
||||
### Path 1: Quick Impact (6 months) - **RECOMMENDED TO START**
|
||||
|
||||
**Timeline**:
|
||||
|
||||
- **Now - Dec 2025**: Write blog post + gather initial feedback
|
||||
- **Dec 2025 - Jan 2026**: Write 4-page WiP paper
|
||||
- **Jan 2026**: Submit to ACM L@S WiP track
|
||||
- **Mar 2026**: Reviews back
|
||||
- **Jun 2026**: Present at L@S (if accepted)
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
1. Blog post explaining the system (for teachers/educators)
|
||||
2. 4-page WiP paper (academic audience)
|
||||
3. Presentation at L@S
|
||||
|
||||
**Effort**: ~40 hours writing + travel to conference
|
||||
|
||||
**Outcome**:
|
||||
|
||||
- Get idea into academic discourse
|
||||
- Receive feedback from learning science researchers
|
||||
- Build credibility for follow-up work
|
||||
|
||||
### Path 2: Full Research Study (12-18 months)
|
||||
|
||||
**Timeline**:
|
||||
|
||||
- **Nov 2025 - Jan 2026**: IRB approval (if university-affiliated)
|
||||
- **Jan - Mar 2026**: Recruit teachers (10-15)
|
||||
- **Mar - May 2026**: Teacher study
|
||||
- Give access to system
|
||||
- Weekly check-ins
|
||||
- Usage log collection
|
||||
- End-of-study interviews
|
||||
- **Jun - Aug 2026**: Analysis + paper writing
|
||||
- **Sep 2026**: Submit to IJAIED or LAK 2027
|
||||
- **2027**: Publication
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
1. IRB protocol + approval
|
||||
2. Teacher recruitment materials
|
||||
3. Interview protocol
|
||||
4. Usage log analysis pipeline
|
||||
5. 25-40 page research paper
|
||||
|
||||
**Effort**: ~200-300 hours + IRB overhead
|
||||
|
||||
**Outcome**:
|
||||
|
||||
- Peer-reviewed empirical research paper
|
||||
- Strong evidence for effectiveness claims
|
||||
- Foundation for future grant proposals
|
||||
|
||||
### Path 3: Open Source + Community (Immediate) - **ALSO RECOMMENDED**
|
||||
|
||||
**Timeline**:
|
||||
|
||||
- **This week**: Write comprehensive blog post
|
||||
- **Ongoing**: Share on HN, Teacher Twitter, EdTech Reddit
|
||||
- **Ongoing**: Respond to feedback, track usage
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
1. Blog post (~2000 words)
|
||||
- Problem statement
|
||||
- System design
|
||||
- How to use it
|
||||
- Theoretical grounding
|
||||
2. Social media campaign
|
||||
3. Outreach to homeschool/teacher communities
|
||||
|
||||
**Effort**: ~20 hours initial + ongoing engagement
|
||||
|
||||
**Outcome**:
|
||||
|
||||
- Organic user base
|
||||
- Real-world feedback
|
||||
- Potential citations/adoption
|
||||
- Informal peer review
|
||||
|
||||
## Recommended Strategy
|
||||
|
||||
**Do Paths 1 + 3 in parallel**:
|
||||
|
||||
1. **This Week** (Path 3):
|
||||
- Write blog post explaining the system
|
||||
- Share widely to get feedback
|
||||
- Start tracking usage/interest
|
||||
|
||||
2. **December 2025** (Path 1):
|
||||
- Draft 4-page WiP paper
|
||||
- Include preliminary feedback from blog responses
|
||||
- Submit to L@S in January
|
||||
|
||||
3. **Spring 2026** (Path 1):
|
||||
- Present at L@S (if accepted)
|
||||
- Get feedback from researchers
|
||||
- Build network in learning sciences
|
||||
|
||||
4. **Summer 2026** (Evaluate):
|
||||
- If system gains users → Path 2 (research study)
|
||||
- If limited adoption → Iterate on design
|
||||
- If strong conference feedback → Target full paper
|
||||
|
||||
## How to Execute: WiP Paper (January 2026)
|
||||
|
||||
### Paper Structure (4 pages)
|
||||
|
||||
**1. Introduction (0.75 pages)**
|
||||
|
||||
- Problem: 1D difficulty conflates challenge and support
|
||||
- Our solution: Constrained 2D space
|
||||
- Contribution: Novel UI paradigm + theoretical framework
|
||||
|
||||
**2. Related Work (0.75 pages)**
|
||||
|
||||
- Intelligent Tutoring Systems (ALEKS, Carnegie Learning)
|
||||
- Adaptive learning platforms (Khan Academy, Duolingo)
|
||||
- Difficulty calibration research (IRT, Elo rating)
|
||||
- Gap: No systems separate challenge from scaffolding
|
||||
|
||||
**3. System Design (1.5 pages)**
|
||||
|
||||
- Hybrid discrete/continuous architecture
|
||||
- Constraint band derivation
|
||||
- Movement modes (both/challenge/support)
|
||||
- Split button UI design
|
||||
- Screenshot of interface
|
||||
|
||||
**4. Theoretical Framework (0.5 pages)**
|
||||
|
||||
- ZPD mapping to constraint band
|
||||
- Cognitive load theory justification
|
||||
- Scaffolding fading principles
|
||||
|
||||
**5. Preliminary Evaluation (0.3 pages)**
|
||||
|
||||
- Your testing experience
|
||||
- Initial teacher feedback (if available)
|
||||
- Identified use cases
|
||||
|
||||
**6. Discussion & Future Work (0.2 pages)**
|
||||
|
||||
- Planned teacher study
|
||||
- Potential for other domains
|
||||
- Limitations and next steps
|
||||
|
||||
### Writing Timeline
|
||||
|
||||
**Week 1 (Dec 2-8)**:
|
||||
|
||||
- Draft sections 1-2 (intro + related work)
|
||||
- Literature search for related systems
|
||||
|
||||
**Week 2 (Dec 9-15)**:
|
||||
|
||||
- Draft section 3 (system design)
|
||||
- Create figures/screenshots
|
||||
|
||||
**Week 3 (Dec 16-22)**:
|
||||
|
||||
- Draft sections 4-6
|
||||
- Get feedback from educator friends
|
||||
|
||||
**Week 4 (Dec 23-Jan 5)**:
|
||||
|
||||
- Revise based on feedback
|
||||
- Polish writing
|
||||
- Format for L@S template
|
||||
|
||||
**Jan 6-10, 2026**:
|
||||
|
||||
- Final read-through
|
||||
- Submit to L@S WiP track
|
||||
|
||||
## How to Execute: Blog Post (This Week)
|
||||
|
||||
### Blog Structure (~2000 words)
|
||||
|
||||
**Title**: "Beyond Easy and Hard: A 2D Approach to Worksheet Difficulty"
|
||||
|
||||
**1. The Problem** (400 words)
|
||||
|
||||
- Teachers need to differentiate instruction
|
||||
- Current tools: "easy/medium/hard" or 1-5 sliders
|
||||
- Real teaching scenario: Student ready for harder problems but still needs visual aids
|
||||
- Can't express this with 1D slider
|
||||
|
||||
**2. Our Solution** (600 words)
|
||||
|
||||
- Two dimensions: Challenge (problem complexity) vs Support (scaffolding)
|
||||
- Constraint band: Not all combinations are pedagogically valid
|
||||
- Split button interface: Default (both) or dimension-specific
|
||||
- Screenshots showing the UI
|
||||
|
||||
**3. Theoretical Grounding** (400 words)
|
||||
|
||||
- Why this maps to learning theory (ZPD, cognitive load)
|
||||
- How constraints encode teaching expertise
|
||||
- Connection to scaffolding fading
|
||||
|
||||
**4. How to Use It** (400 words)
|
||||
|
||||
- Walkthrough: Creating a worksheet
|
||||
- Examples of when to use challenge-only vs support-only
|
||||
- Clicking on the 2D graph (debug feature)
|
||||
|
||||
**5. Try It Yourself** (200 words)
|
||||
|
||||
- Link to live demo
|
||||
- Open source code
|
||||
- Invitation for feedback
|
||||
|
||||
### Distribution Channels
|
||||
|
||||
- **Your blog** (if you have one)
|
||||
- **Medium** (cross-post for reach)
|
||||
- **Hacker News** (Show HN: A 2D difficulty system for math worksheets)
|
||||
- **Reddit**: r/teachers, r/homeschool, r/education
|
||||
- **Twitter/X**: Thread with screenshots
|
||||
- **LinkedIn** (if you're active there)
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Short-term (3 months)
|
||||
|
||||
- [ ] Blog post published and shared
|
||||
- [ ] 50+ teachers try the system
|
||||
- [ ] 5+ pieces of detailed feedback
|
||||
- [ ] WiP paper submitted to L@S
|
||||
|
||||
### Medium-term (1 year)
|
||||
|
||||
- [ ] WiP paper accepted and presented
|
||||
- [ ] 200+ teachers using the system
|
||||
- [ ] Teacher study conducted (if pursuing Path 2)
|
||||
- [ ] Full paper submitted to journal/conference
|
||||
|
||||
### Long-term (2-3 years)
|
||||
|
||||
- [ ] Peer-reviewed research publication
|
||||
- [ ] System adopted by curriculum companies
|
||||
- [ ] Citations from other researchers
|
||||
- [ ] Follow-up studies by other groups
|
||||
|
||||
## Resources Needed
|
||||
|
||||
### For WiP Paper
|
||||
|
||||
- **Time**: ~40 hours writing
|
||||
- **Cost**: Conference registration (~$500-800) + travel (~$1000-2000)
|
||||
- **Skills**: Academic writing (you + me collaborating)
|
||||
|
||||
### For Teacher Study
|
||||
|
||||
- **Time**: ~200-300 hours over 6 months
|
||||
- **Cost**: Teacher incentives ($50/teacher × 15 = $750)
|
||||
- **Skills**: Qualitative research methods
|
||||
- **Optional**: IRB approval (if university-affiliated)
|
||||
|
||||
### For Blog/Outreach
|
||||
|
||||
- **Time**: ~20 hours initial
|
||||
- **Cost**: $0 (all free platforms)
|
||||
- **Skills**: Technical writing, social media engagement
|
||||
|
||||
## Next Steps
|
||||
|
||||
**Immediate (This Week)**:
|
||||
|
||||
1. [ ] Draft blog post outline
|
||||
2. [ ] Take screenshots of the UI in action
|
||||
3. [ ] Create 2-3 usage scenarios with example teacher workflows
|
||||
|
||||
**December 2025**:
|
||||
|
||||
1. [ ] Publish blog post + share widely
|
||||
2. [ ] Start WiP paper draft
|
||||
3. [ ] Conduct literature review for related work
|
||||
|
||||
**January 2026**:
|
||||
|
||||
1. [ ] Complete WiP paper
|
||||
2. [ ] Submit to ACM L@S
|
||||
3. [ ] Evaluate user feedback from blog post
|
||||
|
||||
## Questions to Consider
|
||||
|
||||
1. **Do you have academic affiliation?**
|
||||
- Needed for IRB approval (teacher study)
|
||||
- Some conferences require institutional affiliation
|
||||
- Can collaborate with university researchers if not
|
||||
|
||||
2. **What's your bandwidth?**
|
||||
- WiP paper: ~10 hours/week for 4 weeks
|
||||
- Teacher study: ~10-15 hours/week for 6 months
|
||||
- Blog post: ~10 hours total
|
||||
|
||||
3. **What's your goal?**
|
||||
- Academic credibility → Prioritize WiP paper
|
||||
- Real-world impact → Prioritize blog + outreach
|
||||
- Research career → Prioritize full study
|
||||
- All of the above → Do Path 1 + 3, then evaluate
|
||||
|
||||
4. **Do you want to recruit teachers now?**
|
||||
- Could start informal study alongside blog post
|
||||
- Interview 5-10 teachers who use the system
|
||||
- Include in WiP paper as preliminary findings
|
||||
|
||||
## Conclusion
|
||||
|
||||
We have a genuinely novel contribution that combines:
|
||||
|
||||
- **Theoretical rigor** (learning science foundations)
|
||||
- **Technical innovation** (constrained 2D space + hybrid architecture)
|
||||
- **Practical utility** (working system teachers can use today)
|
||||
|
||||
This is publishable material. The question is timeline and effort:
|
||||
|
||||
- **Lowest effort**: Blog post + social sharing (~20 hours)
|
||||
- **Medium effort**: Blog + WiP paper (~60 hours + travel)
|
||||
- **High effort**: Full research study (~300 hours over 18 months)
|
||||
|
||||
**My recommendation**: Start with blog + WiP paper (Paths 1 + 3). This gets the idea into academic circulation with minimal risk, while building the foundation for a larger study if the system gains traction.
|
||||
|
||||
Would you like help drafting the blog post or WiP paper outline?
|
||||
@@ -1,538 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { PageWithNav } from "@/components/PageWithNav";
|
||||
import { css } from "../../../../../../styled-system/css";
|
||||
import {
|
||||
container,
|
||||
grid,
|
||||
hstack,
|
||||
stack,
|
||||
} from "../../../../../../styled-system/patterns";
|
||||
import { ConfigPanel } from "./ConfigPanel";
|
||||
import { WorksheetPreview } from "./WorksheetPreview";
|
||||
import type { WorksheetFormState } from "../types";
|
||||
import { validateWorksheetConfig } from "../validation";
|
||||
|
||||
type GenerationStatus = "idle" | "generating" | "error";
|
||||
|
||||
/**
|
||||
* Get current date formatted as "Month Day, Year"
|
||||
*/
|
||||
function getDefaultDate(): string {
|
||||
const now = new Date();
|
||||
return now.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
interface AdditionWorksheetClientProps {
|
||||
initialSettings: Omit<WorksheetFormState, "date" | "rows" | "total">;
|
||||
initialPreview?: string[];
|
||||
}
|
||||
|
||||
export function AdditionWorksheetClient({
|
||||
initialSettings,
|
||||
initialPreview,
|
||||
}: AdditionWorksheetClientProps) {
|
||||
console.log("[Worksheet Client] Component render, initialSettings:", {
|
||||
problemsPerPage: initialSettings.problemsPerPage,
|
||||
cols: initialSettings.cols,
|
||||
pages: initialSettings.pages,
|
||||
seed: initialSettings.seed,
|
||||
});
|
||||
|
||||
const t = useTranslations("create.worksheets.addition");
|
||||
const [generationStatus, setGenerationStatus] =
|
||||
useState<GenerationStatus>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Calculate derived state from initial settings
|
||||
// Use defaults for required fields (server should always provide these, but TypeScript needs guarantees)
|
||||
const problemsPerPage = initialSettings.problemsPerPage ?? 20;
|
||||
const pages = initialSettings.pages ?? 1;
|
||||
const cols = initialSettings.cols ?? 5;
|
||||
|
||||
const rows = Math.ceil((problemsPerPage * pages) / cols);
|
||||
const total = problemsPerPage * pages;
|
||||
|
||||
// Immediate form state (for controls - updates instantly)
|
||||
const [formState, setFormState] = useState<WorksheetFormState>(() => {
|
||||
const initial = {
|
||||
...initialSettings,
|
||||
rows,
|
||||
total,
|
||||
date: "", // Will be set at generation time
|
||||
// seed comes from initialSettings (server-generated, stable across StrictMode remounts)
|
||||
};
|
||||
console.log("[Worksheet Client] Initial formState:", {
|
||||
seed: initial.seed,
|
||||
});
|
||||
return initial;
|
||||
});
|
||||
|
||||
// Debounced form state (for preview - updates after delay)
|
||||
const [debouncedFormState, setDebouncedFormState] =
|
||||
useState<WorksheetFormState>(() => {
|
||||
console.log(
|
||||
"[Worksheet Client] Initial debouncedFormState (same as formState)",
|
||||
);
|
||||
return formState;
|
||||
});
|
||||
|
||||
// Store the previous formState to detect real changes
|
||||
const prevFormStateRef = React.useRef(formState);
|
||||
|
||||
// Log whenever debouncedFormState changes (this triggers preview re-fetch)
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[Worksheet Client] debouncedFormState changed - preview will re-fetch:",
|
||||
{
|
||||
seed: debouncedFormState.seed,
|
||||
problemsPerPage: debouncedFormState.problemsPerPage,
|
||||
},
|
||||
);
|
||||
}, [debouncedFormState]);
|
||||
|
||||
// Debounce preview updates (500ms delay) - only when formState actually changes
|
||||
useEffect(() => {
|
||||
console.log("[Debounce Effect] Triggered");
|
||||
console.log("[Debounce Effect] Current formState seed:", formState.seed);
|
||||
console.log(
|
||||
"[Debounce Effect] Previous formState seed:",
|
||||
prevFormStateRef.current.seed,
|
||||
);
|
||||
|
||||
// Skip if formState hasn't actually changed (handles StrictMode double-render)
|
||||
if (formState === prevFormStateRef.current) {
|
||||
console.log("[Debounce Effect] Skipping - formState reference unchanged");
|
||||
return;
|
||||
}
|
||||
|
||||
prevFormStateRef.current = formState;
|
||||
|
||||
console.log(
|
||||
"[Debounce Effect] Setting timer to update debouncedFormState in 500ms",
|
||||
);
|
||||
const timer = setTimeout(() => {
|
||||
console.log(
|
||||
"[Debounce Effect] Timer fired - updating debouncedFormState",
|
||||
);
|
||||
setDebouncedFormState(formState);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
console.log("[Debounce Effect] Cleanup - clearing timer");
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [formState]);
|
||||
|
||||
// Store the previous formState for auto-save to detect real changes
|
||||
const prevAutoSaveFormStateRef = React.useRef(formState);
|
||||
|
||||
// Auto-save settings when they change (debounced) - skip on initial mount
|
||||
useEffect(() => {
|
||||
// Skip auto-save if formState hasn't actually changed (handles StrictMode double-render)
|
||||
if (formState === prevAutoSaveFormStateRef.current) {
|
||||
console.log(
|
||||
"[Worksheet Settings] Skipping auto-save - formState reference unchanged",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
prevAutoSaveFormStateRef.current = formState;
|
||||
|
||||
console.log("[Worksheet Settings] Settings changed, will save in 1s...");
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
console.log("[Worksheet Settings] Attempting to save settings...");
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// Extract only the fields we want to persist (exclude date, seed, derived state)
|
||||
const {
|
||||
problemsPerPage,
|
||||
cols,
|
||||
pages,
|
||||
orientation,
|
||||
name,
|
||||
pAnyStart,
|
||||
pAllStart,
|
||||
interpolate,
|
||||
showCarryBoxes,
|
||||
showAnswerBoxes,
|
||||
showPlaceValueColors,
|
||||
showProblemNumbers,
|
||||
showCellBorder,
|
||||
showTenFrames,
|
||||
showTenFramesForAll,
|
||||
fontSize,
|
||||
} = formState;
|
||||
|
||||
const response = await fetch("/api/worksheets/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
type: "addition",
|
||||
config: {
|
||||
problemsPerPage,
|
||||
cols,
|
||||
pages,
|
||||
orientation,
|
||||
name,
|
||||
pAnyStart,
|
||||
pAllStart,
|
||||
interpolate,
|
||||
showCarryBoxes,
|
||||
showAnswerBoxes,
|
||||
showPlaceValueColors,
|
||||
showProblemNumbers,
|
||||
showCellBorder,
|
||||
showTenFrames,
|
||||
showTenFramesForAll,
|
||||
fontSize,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log("[Worksheet Settings] Save response:", data);
|
||||
if (data.success) {
|
||||
console.log("[Worksheet Settings] ✓ Settings saved successfully");
|
||||
setLastSaved(new Date());
|
||||
} else {
|
||||
console.log("[Worksheet Settings] Save skipped");
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
"[Worksheet Settings] Save failed with status:",
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - settings persistence is not critical
|
||||
console.error("[Worksheet Settings] Settings save error:", error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, 1000); // 1 second debounce for auto-save
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [formState]);
|
||||
|
||||
const handleFormChange = (updates: Partial<WorksheetFormState>) => {
|
||||
setFormState((prev) => {
|
||||
const newState = { ...prev, ...updates };
|
||||
|
||||
// Generate new seed when problem settings change
|
||||
const affectsProblems =
|
||||
updates.problemsPerPage !== undefined ||
|
||||
updates.cols !== undefined ||
|
||||
updates.pages !== undefined ||
|
||||
updates.orientation !== undefined ||
|
||||
updates.pAnyStart !== undefined ||
|
||||
updates.pAllStart !== undefined ||
|
||||
updates.interpolate !== undefined;
|
||||
|
||||
if (affectsProblems) {
|
||||
newState.seed = Date.now() % 2147483647;
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerationStatus("generating");
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Set current date at generation time
|
||||
const configWithDate = {
|
||||
...formState,
|
||||
date: getDefaultDate(),
|
||||
};
|
||||
|
||||
// Validate configuration
|
||||
const validation = validateWorksheetConfig(configWithDate);
|
||||
if (!validation.isValid || !validation.config) {
|
||||
throw new Error(
|
||||
validation.errors?.join(", ") || "Invalid configuration",
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch("/api/create/worksheets/addition", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(configWithDate),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorResult = await response.json();
|
||||
const errorMsg = errorResult.details
|
||||
? `${errorResult.error}\n\n${errorResult.details}`
|
||||
: errorResult.error || "Generation failed";
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// Success - response is binary PDF data, trigger download
|
||||
const blob = await response.blob();
|
||||
const filename = `addition-worksheet-${formState.name || "student"}-${Date.now()}.pdf`;
|
||||
|
||||
// Create download link and trigger download
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
setGenerationStatus("idle");
|
||||
} catch (err) {
|
||||
console.error("Generation error:", err);
|
||||
setError(err instanceof Error ? err.message : "Unknown error occurred");
|
||||
setGenerationStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewGeneration = () => {
|
||||
setGenerationStatus("idle");
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle={t("navTitle")} navEmoji="📝">
|
||||
<div
|
||||
data-component="addition-worksheet-page"
|
||||
className={css({ minHeight: "100vh", bg: "gray.50" })}
|
||||
>
|
||||
{/* Main Content */}
|
||||
<div className={container({ maxW: "7xl", px: "4", py: "8" })}>
|
||||
<div className={stack({ gap: "6", mb: "8" })}>
|
||||
<div className={stack({ gap: "2", textAlign: "center" })}>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: "3xl",
|
||||
fontWeight: "bold",
|
||||
color: "gray.900",
|
||||
})}
|
||||
>
|
||||
{t("pageTitle")}
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: "lg",
|
||||
color: "gray.600",
|
||||
})}
|
||||
>
|
||||
{t("pageSubtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Interface */}
|
||||
<div
|
||||
className={grid({
|
||||
columns: { base: 1, lg: 2 },
|
||||
gap: "8",
|
||||
alignItems: "start",
|
||||
})}
|
||||
>
|
||||
{/* Configuration Panel */}
|
||||
<div className={stack({ gap: "3" })}>
|
||||
<div
|
||||
data-section="config-panel"
|
||||
className={css({
|
||||
bg: "white",
|
||||
rounded: "2xl",
|
||||
shadow: "card",
|
||||
p: "8",
|
||||
})}
|
||||
>
|
||||
<ConfigPanel
|
||||
formState={formState}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Settings saved indicator */}
|
||||
<div
|
||||
data-element="settings-status"
|
||||
className={css({
|
||||
fontSize: "sm",
|
||||
color: "gray.600",
|
||||
textAlign: "center",
|
||||
py: "2",
|
||||
})}
|
||||
>
|
||||
{isSaving ? (
|
||||
<span className={css({ color: "gray.500" })}>
|
||||
Saving settings...
|
||||
</span>
|
||||
) : lastSaved ? (
|
||||
<span className={css({ color: "green.600" })}>
|
||||
✓ Settings saved at {lastSaved.toLocaleTimeString()}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview & Generate Panel */}
|
||||
<div className={stack({ gap: "8" })}>
|
||||
{/* Preview */}
|
||||
<div
|
||||
data-section="preview-panel"
|
||||
className={css({
|
||||
bg: "white",
|
||||
rounded: "2xl",
|
||||
shadow: "card",
|
||||
p: "6",
|
||||
})}
|
||||
>
|
||||
<WorksheetPreview
|
||||
formState={debouncedFormState}
|
||||
initialData={initialPreview}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<div
|
||||
data-section="generate-panel"
|
||||
className={css({
|
||||
bg: "white",
|
||||
rounded: "2xl",
|
||||
shadow: "card",
|
||||
p: "6",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
data-action="generate-worksheet"
|
||||
onClick={handleGenerate}
|
||||
disabled={generationStatus === "generating"}
|
||||
className={css({
|
||||
w: "full",
|
||||
px: "6",
|
||||
py: "4",
|
||||
bg: "brand.600",
|
||||
color: "white",
|
||||
fontSize: "lg",
|
||||
fontWeight: "semibold",
|
||||
rounded: "xl",
|
||||
shadow: "card",
|
||||
transition: "all",
|
||||
cursor:
|
||||
generationStatus === "generating"
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
opacity: generationStatus === "generating" ? "0.7" : "1",
|
||||
_hover:
|
||||
generationStatus === "generating"
|
||||
? {}
|
||||
: {
|
||||
bg: "brand.700",
|
||||
transform: "translateY(-1px)",
|
||||
shadow: "modal",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className={hstack({ gap: "3", justify: "center" })}>
|
||||
{generationStatus === "generating" ? (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
w: "5",
|
||||
h: "5",
|
||||
border: "2px solid",
|
||||
borderColor: "white",
|
||||
borderTopColor: "transparent",
|
||||
rounded: "full",
|
||||
animation: "spin 1s linear infinite",
|
||||
})}
|
||||
/>
|
||||
{t("generate.generating")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={css({ fontSize: "xl" })}>📝</div>
|
||||
{t("generate.button")}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{generationStatus === "error" && error && (
|
||||
<div
|
||||
data-status="error"
|
||||
className={css({
|
||||
bg: "red.50",
|
||||
border: "1px solid",
|
||||
borderColor: "red.200",
|
||||
rounded: "2xl",
|
||||
p: "8",
|
||||
mt: "8",
|
||||
})}
|
||||
>
|
||||
<div className={stack({ gap: "4" })}>
|
||||
<div className={hstack({ gap: "3", alignItems: "center" })}>
|
||||
<div className={css({ fontSize: "2xl" })}>❌</div>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: "xl",
|
||||
fontWeight: "semibold",
|
||||
color: "red.800",
|
||||
})}
|
||||
>
|
||||
{t("error.title")}
|
||||
</h3>
|
||||
</div>
|
||||
<pre
|
||||
className={css({
|
||||
color: "red.700",
|
||||
lineHeight: "relaxed",
|
||||
whiteSpace: "pre-wrap",
|
||||
fontFamily: "mono",
|
||||
fontSize: "sm",
|
||||
overflowX: "auto",
|
||||
})}
|
||||
>
|
||||
{error}
|
||||
</pre>
|
||||
<button
|
||||
data-action="try-again"
|
||||
onClick={handleNewGeneration}
|
||||
className={css({
|
||||
alignSelf: "start",
|
||||
px: "4",
|
||||
py: "2",
|
||||
bg: "red.600",
|
||||
color: "white",
|
||||
fontWeight: "medium",
|
||||
rounded: "lg",
|
||||
transition: "all",
|
||||
_hover: { bg: "red.700" },
|
||||
})}
|
||||
>
|
||||
{t("error.tryAgain")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
);
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { css } from '../../../../../../styled-system/css'
|
||||
import type { WorksheetFormState } from '../types'
|
||||
|
||||
interface DisplayOptionsPreviewProps {
|
||||
formState: WorksheetFormState
|
||||
}
|
||||
|
||||
interface MathSentenceProps {
|
||||
operands: number[]
|
||||
operator: string
|
||||
onChange: (operands: number[]) => void
|
||||
labels?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexible math sentence component supporting operators with arity 1-3
|
||||
* Examples:
|
||||
* Arity 1 (unary): [64] with "√" → "√64"
|
||||
* Arity 2 (binary): [45, 27] with "+" → "45 + 27"
|
||||
* Arity 3 (ternary): [5, 10, 15] with "between" → "5 < 10 < 15"
|
||||
*/
|
||||
function MathSentence({ operands, operator, onChange, labels }: MathSentenceProps) {
|
||||
const handleOperandChange = (index: number, value: string) => {
|
||||
const numValue = Number.parseInt(value, 10)
|
||||
if (!Number.isNaN(numValue) && numValue >= 0 && numValue <= 99) {
|
||||
const newOperands = [...operands]
|
||||
newOperands[index] = numValue
|
||||
onChange(newOperands)
|
||||
}
|
||||
}
|
||||
|
||||
const renderInput = (value: number, index: number) => (
|
||||
<input
|
||||
key={index}
|
||||
type="number"
|
||||
min="0"
|
||||
max="99"
|
||||
value={value}
|
||||
onChange={(e) => handleOperandChange(index, e.target.value)}
|
||||
aria-label={labels?.[index] || `operand ${index + 1}`}
|
||||
className={css({
|
||||
width: '3.5em',
|
||||
px: '1',
|
||||
py: '0.5',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
textAlign: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: 'transparent',
|
||||
rounded: 'sm',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s',
|
||||
_hover: {
|
||||
borderColor: 'gray.300',
|
||||
},
|
||||
_focus: {
|
||||
borderColor: 'brand.500',
|
||||
ring: '1px',
|
||||
ringColor: 'brand.200',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
|
||||
// Render based on arity
|
||||
if (operands.length === 1) {
|
||||
// Unary operator (prefix): √64 or ±5
|
||||
return (
|
||||
<div
|
||||
data-component="math-sentence"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
<span>{operator}</span>
|
||||
{renderInput(operands[0], 0)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (operands.length === 2) {
|
||||
// Binary operator (infix): 45 + 27
|
||||
return (
|
||||
<div
|
||||
data-component="math-sentence"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
{renderInput(operands[0], 0)}
|
||||
<span>{operator}</span>
|
||||
{renderInput(operands[1], 1)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (operands.length === 3) {
|
||||
// Ternary operator: 5 < 10 < 15 or similar
|
||||
return (
|
||||
<div
|
||||
data-component="math-sentence"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
{renderInput(operands[0], 0)}
|
||||
<span>{operator}</span>
|
||||
{renderInput(operands[1], 1)}
|
||||
<span>{operator}</span>
|
||||
{renderInput(operands[2], 2)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function fetchExample(options: {
|
||||
showCarryBoxes: boolean
|
||||
showAnswerBoxes: boolean
|
||||
showPlaceValueColors: boolean
|
||||
showProblemNumbers: boolean
|
||||
showCellBorder: boolean
|
||||
showTenFrames: boolean
|
||||
showTenFramesForAll: boolean
|
||||
addend1: number
|
||||
addend2: number
|
||||
}): Promise<string> {
|
||||
const response = await fetch('/api/create/worksheets/addition/example', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...options,
|
||||
fontSize: 16,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch example')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.svg
|
||||
}
|
||||
|
||||
export function DisplayOptionsPreview({ formState }: DisplayOptionsPreviewProps) {
|
||||
// Local state for operands (not debounced - we want immediate feedback)
|
||||
const [operands, setOperands] = useState([45, 27])
|
||||
|
||||
// Debounce the display options to avoid hammering the server
|
||||
const [debouncedOptions, setDebouncedOptions] = useState({
|
||||
showCarryBoxes: formState.showCarryBoxes ?? true,
|
||||
showAnswerBoxes: formState.showAnswerBoxes ?? true,
|
||||
showPlaceValueColors: formState.showPlaceValueColors ?? true,
|
||||
showProblemNumbers: formState.showProblemNumbers ?? true,
|
||||
showCellBorder: formState.showCellBorder ?? true,
|
||||
showTenFrames: formState.showTenFrames ?? false,
|
||||
showTenFramesForAll: formState.showTenFramesForAll ?? false,
|
||||
addend1: operands[0],
|
||||
addend2: operands[1],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedOptions({
|
||||
showCarryBoxes: formState.showCarryBoxes ?? true,
|
||||
showAnswerBoxes: formState.showAnswerBoxes ?? true,
|
||||
showPlaceValueColors: formState.showPlaceValueColors ?? true,
|
||||
showProblemNumbers: formState.showProblemNumbers ?? true,
|
||||
showCellBorder: formState.showCellBorder ?? true,
|
||||
showTenFrames: formState.showTenFrames ?? false,
|
||||
showTenFramesForAll: formState.showTenFramesForAll ?? false,
|
||||
addend1: operands[0],
|
||||
addend2: operands[1],
|
||||
})
|
||||
}, 300) // 300ms debounce
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [
|
||||
formState.showCarryBoxes,
|
||||
formState.showAnswerBoxes,
|
||||
formState.showPlaceValueColors,
|
||||
formState.showProblemNumbers,
|
||||
formState.showCellBorder,
|
||||
formState.showTenFrames,
|
||||
formState.showTenFramesForAll,
|
||||
operands,
|
||||
])
|
||||
|
||||
const { data: svg, isLoading } = useQuery({
|
||||
queryKey: ['display-example', debouncedOptions],
|
||||
queryFn: () => fetchExample(debouncedOptions),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="display-options-preview"
|
||||
className={css({
|
||||
p: '3',
|
||||
bg: 'white',
|
||||
rounded: 'xl',
|
||||
border: '2px solid',
|
||||
borderColor: 'brand.200',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2',
|
||||
width: 'fit-content',
|
||||
maxWidth: '100%',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'semibold',
|
||||
color: 'gray.500',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
})}
|
||||
>
|
||||
Preview
|
||||
</div>
|
||||
<MathSentence
|
||||
operands={operands}
|
||||
operator="+"
|
||||
onChange={setOperands}
|
||||
labels={['addend', 'addend']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minH: '200px',
|
||||
color: 'gray.400',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
Generating preview...
|
||||
</div>
|
||||
) : svg ? (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minH: '200px',
|
||||
'& svg': {
|
||||
maxW: 'full',
|
||||
h: 'auto',
|
||||
},
|
||||
})}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useState, useEffect, useRef } from 'react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../../styled-system/css'
|
||||
import { hstack, stack } from '../../../../../../styled-system/patterns'
|
||||
import type { WorksheetFormState } from '../types'
|
||||
|
||||
interface WorksheetPreviewProps {
|
||||
formState: WorksheetFormState
|
||||
initialData?: string[]
|
||||
}
|
||||
|
||||
function getDefaultDate(): string {
|
||||
const now = new Date()
|
||||
return now.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchWorksheetPreview(formState: WorksheetFormState): Promise<string[]> {
|
||||
const fetchId = Math.random().toString(36).slice(2, 9)
|
||||
console.log(`[WorksheetPreview] fetchWorksheetPreview called (ID: ${fetchId})`, {
|
||||
seed: formState.seed,
|
||||
problemsPerPage: formState.problemsPerPage,
|
||||
})
|
||||
|
||||
// Set current date for preview
|
||||
const configWithDate = {
|
||||
...formState,
|
||||
date: getDefaultDate(),
|
||||
}
|
||||
|
||||
// Use absolute URL for SSR compatibility
|
||||
const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'
|
||||
const url = `${baseUrl}/api/create/worksheets/addition/preview`
|
||||
|
||||
console.log(`[WorksheetPreview] Fetching from API (ID: ${fetchId})...`)
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(configWithDate),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMsg = errorData.error || errorData.message || 'Failed to fetch preview'
|
||||
const details = errorData.details ? `\n\n${errorData.details}` : ''
|
||||
const errors = errorData.errors ? `\n\nErrors:\n${errorData.errors.join('\n')}` : ''
|
||||
throw new Error(errorMsg + details + errors)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log(`[WorksheetPreview] Fetch complete (ID: ${fetchId}), pages:`, data.pages.length)
|
||||
return data.pages
|
||||
}
|
||||
|
||||
function PreviewContent({ formState, initialData }: WorksheetPreviewProps) {
|
||||
const t = useTranslations('create.worksheets.addition')
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
|
||||
// Track if we've used the initial data (so we only use it once)
|
||||
const initialDataUsed = useRef(false)
|
||||
|
||||
console.log('[WorksheetPreview] Rendering with formState:', {
|
||||
seed: formState.seed,
|
||||
problemsPerPage: formState.problemsPerPage,
|
||||
hasInitialData: !!initialData,
|
||||
initialDataUsed: initialDataUsed.current,
|
||||
})
|
||||
|
||||
// Only use initialData on the very first query, not on subsequent fetches
|
||||
const queryInitialData = !initialDataUsed.current && initialData ? initialData : undefined
|
||||
|
||||
if (queryInitialData) {
|
||||
console.log('[WorksheetPreview] Using server-generated initial data')
|
||||
initialDataUsed.current = true
|
||||
}
|
||||
|
||||
// Use Suspense Query - will suspend during loading
|
||||
const { data: pages } = useSuspenseQuery({
|
||||
queryKey: [
|
||||
'worksheet-preview',
|
||||
// PRIMARY state
|
||||
formState.problemsPerPage,
|
||||
formState.cols,
|
||||
formState.pages,
|
||||
formState.orientation,
|
||||
// Other settings that affect appearance
|
||||
formState.name,
|
||||
formState.pAnyStart,
|
||||
formState.pAllStart,
|
||||
formState.interpolate,
|
||||
formState.showCarryBoxes,
|
||||
formState.showAnswerBoxes,
|
||||
formState.showPlaceValueColors,
|
||||
formState.showProblemNumbers,
|
||||
formState.showCellBorder,
|
||||
formState.showTenFrames,
|
||||
formState.showTenFramesForAll,
|
||||
formState.seed, // Include seed to bust cache when problem set regenerates
|
||||
// Note: fontSize, date, rows, total intentionally excluded
|
||||
// (rows and total are derived from primary state)
|
||||
],
|
||||
queryFn: () => {
|
||||
console.log('[WorksheetPreview] Fetching preview from API...')
|
||||
return fetchWorksheetPreview(formState)
|
||||
},
|
||||
initialData: queryInitialData, // Only use on first render
|
||||
})
|
||||
|
||||
console.log('[WorksheetPreview] Preview fetched, pages:', pages.length)
|
||||
|
||||
const totalPages = pages.length
|
||||
|
||||
// Reset to first page when preview updates
|
||||
useEffect(() => {
|
||||
setCurrentPage(0)
|
||||
}, [pages])
|
||||
|
||||
return (
|
||||
<div data-component="worksheet-preview" className={stack({ gap: '4' })}>
|
||||
<div className={stack({ gap: '1' })}>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('preview.title')}
|
||||
</h3>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.600',
|
||||
})}
|
||||
>
|
||||
{totalPages > 1 ? `${totalPages} pages` : t('preview.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls (top) */}
|
||||
{totalPages > 1 && (
|
||||
<div
|
||||
className={hstack({
|
||||
gap: '3',
|
||||
justify: 'center',
|
||||
align: 'center',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
|
||||
disabled={currentPage === 0}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
cursor: 'pointer',
|
||||
_disabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.700',
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
Page {currentPage + 1} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
cursor: 'pointer',
|
||||
_disabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SVG Preview */}
|
||||
<div
|
||||
data-element="svg-preview"
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'lg',
|
||||
p: '4',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
'& svg': {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '70vh',
|
||||
height: 'auto',
|
||||
width: 'auto',
|
||||
},
|
||||
})}
|
||||
dangerouslySetInnerHTML={{ __html: pages[currentPage] }}
|
||||
/>
|
||||
|
||||
{/* Pagination Controls (bottom) */}
|
||||
{totalPages > 1 && (
|
||||
<div
|
||||
className={hstack({
|
||||
gap: '3',
|
||||
justify: 'center',
|
||||
align: 'center',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
|
||||
disabled={currentPage === 0}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
cursor: 'pointer',
|
||||
_disabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.700',
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
Page {currentPage + 1} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
cursor: 'pointer',
|
||||
_disabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info about full worksheet */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'blue.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'blue.200',
|
||||
rounded: 'lg',
|
||||
p: '3',
|
||||
fontSize: 'sm',
|
||||
color: 'blue.800',
|
||||
})}
|
||||
>
|
||||
<strong>Full worksheet:</strong> {formState.total} problems in a {formState.cols}×
|
||||
{formState.rows} grid
|
||||
{formState.interpolate && ' (progressive difficulty: easy → hard)'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewFallback() {
|
||||
console.log('[WorksheetPreview] Showing fallback (Suspense boundary)')
|
||||
return (
|
||||
<div
|
||||
data-component="worksheet-preview-loading"
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
p: '6',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '600px',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
color: 'gray.400',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
Generating preview...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorksheetPreview({ formState, initialData }: WorksheetPreviewProps) {
|
||||
return (
|
||||
<Suspense fallback={<PreviewFallback />}>
|
||||
<PreviewContent formState={formState} initialData={initialData} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Shared logic for generating worksheet previews (used by both API route and SSR)
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { validateWorksheetConfig } from './validation'
|
||||
import { generateProblems } from './problemGenerator'
|
||||
import { generateTypstSource } from './typstGenerator'
|
||||
import type { WorksheetFormState } from './types'
|
||||
|
||||
export interface PreviewResult {
|
||||
success: boolean
|
||||
pages?: string[]
|
||||
error?: string
|
||||
details?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate worksheet preview SVG pages
|
||||
* Can be called from API routes or Server Components
|
||||
*/
|
||||
export function generateWorksheetPreview(config: WorksheetFormState): PreviewResult {
|
||||
try {
|
||||
// Validate configuration
|
||||
const validation = validateWorksheetConfig(config)
|
||||
if (!validation.isValid || !validation.config) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid configuration',
|
||||
details: validation.errors?.join(', '),
|
||||
}
|
||||
}
|
||||
|
||||
const validatedConfig = validation.config
|
||||
|
||||
// Generate all problems for full preview
|
||||
const problems = generateProblems(
|
||||
validatedConfig.total,
|
||||
validatedConfig.pAnyStart,
|
||||
validatedConfig.pAllStart,
|
||||
validatedConfig.interpolate,
|
||||
validatedConfig.seed
|
||||
)
|
||||
|
||||
// Generate Typst sources (one per page)
|
||||
const typstSources = generateTypstSource(validatedConfig, problems)
|
||||
|
||||
// Compile each page source to SVG (using stdout for single-page output)
|
||||
const pages: string[] = []
|
||||
for (let i = 0; i < typstSources.length; i++) {
|
||||
const typstSource = typstSources[i]
|
||||
|
||||
// Compile to SVG via stdin/stdout
|
||||
try {
|
||||
const svgOutput = execSync('typst compile --format svg - -', {
|
||||
input: typstSource,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB limit
|
||||
})
|
||||
pages.push(svgOutput)
|
||||
} catch (error) {
|
||||
console.error(`Typst compilation error (page ${i + 1}):`, error)
|
||||
|
||||
// Extract the actual Typst error message
|
||||
const stderr =
|
||||
error instanceof Error && 'stderr' in error
|
||||
? String((error as any).stderr)
|
||||
: 'Unknown compilation error'
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to compile preview (page ${i + 1})`,
|
||||
details: stderr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
pages,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error)
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to generate preview',
|
||||
details: errorMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { db, schema } from '@/db'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { parseAdditionConfig, defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
|
||||
import { AdditionWorksheetClient } from './components/AdditionWorksheetClient'
|
||||
import type { WorksheetFormState } from './types'
|
||||
import { generateWorksheetPreview } from './generatePreview'
|
||||
|
||||
/**
|
||||
* Get current date formatted as "Month Day, Year"
|
||||
*/
|
||||
function getDefaultDate(): string {
|
||||
const now = new Date()
|
||||
return now.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load worksheet settings from database (server-side)
|
||||
*/
|
||||
async function loadWorksheetSettings(): Promise<
|
||||
Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
|
||||
> {
|
||||
try {
|
||||
const viewerId = await getViewerId()
|
||||
|
||||
// Look up user's saved settings
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(schema.worksheetSettings)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.worksheetSettings.userId, viewerId),
|
||||
eq(schema.worksheetSettings.worksheetType, 'addition')
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!row) {
|
||||
// No saved settings, return defaults with a stable seed
|
||||
return {
|
||||
...defaultAdditionConfig,
|
||||
seed: Date.now() % 2147483647,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and validate config (auto-migrates to latest version)
|
||||
const config = parseAdditionConfig(row.config)
|
||||
return {
|
||||
...config,
|
||||
seed: Date.now() % 2147483647,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load worksheet settings:', error)
|
||||
// Return defaults on error with a stable seed
|
||||
return {
|
||||
...defaultAdditionConfig,
|
||||
seed: Date.now() % 2147483647,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AdditionWorksheetPage() {
|
||||
const initialSettings = await loadWorksheetSettings()
|
||||
|
||||
// Calculate derived state needed for preview
|
||||
const rows = Math.ceil(
|
||||
(initialSettings.problemsPerPage * initialSettings.pages) / initialSettings.cols
|
||||
)
|
||||
const total = initialSettings.problemsPerPage * initialSettings.pages
|
||||
|
||||
// Create full config for preview generation
|
||||
const fullConfig: WorksheetFormState = {
|
||||
...initialSettings,
|
||||
rows,
|
||||
total,
|
||||
date: getDefaultDate(),
|
||||
}
|
||||
|
||||
// Pre-generate worksheet preview on the server
|
||||
console.log('[SSR] Generating worksheet preview on server...')
|
||||
const previewResult = generateWorksheetPreview(fullConfig)
|
||||
console.log('[SSR] Preview generation complete:', previewResult.success ? 'success' : 'failed')
|
||||
|
||||
// Pass settings and preview to client
|
||||
return (
|
||||
<AdditionWorksheetClient
|
||||
initialSettings={initialSettings}
|
||||
initialPreview={previewResult.success ? previewResult.pages : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
// Problem generation logic for double-digit addition worksheets
|
||||
|
||||
import type { AdditionProblem, ProblemCategory } from './types'
|
||||
|
||||
/**
|
||||
* Mulberry32 PRNG for reproducible random number generation
|
||||
*/
|
||||
export function createPRNG(seed: number) {
|
||||
let state = seed
|
||||
return function rand(): number {
|
||||
let t = (state += 0x6d2b79f5)
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1)
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a random element from an array
|
||||
*/
|
||||
function pick<T>(arr: T[], rand: () => number): T {
|
||||
return arr[Math.floor(rand() * arr.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random integer between min and max (inclusive)
|
||||
*/
|
||||
function randint(min: number, max: number, rand: () => number): number {
|
||||
return Math.floor(rand() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random two-digit number (10-99)
|
||||
*/
|
||||
function twoDigit(rand: () => number): number {
|
||||
const tens = randint(1, 9, rand)
|
||||
const ones = randint(0, 9, rand)
|
||||
return tens * 10 + ones
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a problem with NO regrouping
|
||||
* (ones sum < 10 AND tens sum < 10)
|
||||
*/
|
||||
export function generateNonRegroup(rand: () => number): [number, number] {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
const aT = Math.floor((a % 100) / 10)
|
||||
const aO = a % 10
|
||||
const bT = Math.floor((b % 100) / 10)
|
||||
const bO = b % 10
|
||||
|
||||
if (aO + bO < 10 && aT + bT < 10) {
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return [12, 34]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a problem with regrouping in ONES only
|
||||
* (ones sum >= 10 AND tens sum + carry < 10)
|
||||
*/
|
||||
export function generateOnesOnly(rand: () => number): [number, number] {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
const aT = Math.floor((a % 100) / 10)
|
||||
const aO = a % 10
|
||||
const bT = Math.floor((b % 100) / 10)
|
||||
const bO = b % 10
|
||||
|
||||
if (aO + bO >= 10 && aT + bT + 1 < 10) {
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return [58, 31]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a problem with regrouping in BOTH ones and tens
|
||||
* (ones sum >= 10 AND tens sum + carry >= 10)
|
||||
*/
|
||||
export function generateBoth(rand: () => number): [number, number] {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
const aT = Math.floor((a % 100) / 10)
|
||||
const aO = a % 10
|
||||
const bT = Math.floor((b % 100) / 10)
|
||||
const bO = b % 10
|
||||
|
||||
if (aO + bO >= 10 && aT + bT + 1 >= 10) {
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return [68, 47]
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add a unique problem to the list
|
||||
* Returns true if added, false if duplicate
|
||||
*/
|
||||
function uniquePush(list: AdditionProblem[], a: number, b: number, seen: Set<string>): boolean {
|
||||
const key = [Math.min(a, b), Math.max(a, b)].join('+')
|
||||
if (seen.has(key) || a === b) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
list.push({ a, b })
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete set of problems based on difficulty parameters
|
||||
*/
|
||||
export function generateProblems(
|
||||
total: number,
|
||||
pAnyStart: number,
|
||||
pAllStart: number,
|
||||
interpolate: boolean,
|
||||
seed: number
|
||||
): AdditionProblem[] {
|
||||
const rand = createPRNG(seed)
|
||||
const problems: AdditionProblem[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
// Calculate position from start (0) to end (1)
|
||||
const frac = total <= 1 ? 0 : i / (total - 1)
|
||||
// Progressive difficulty: start easy, end hard
|
||||
const difficultyMultiplier = interpolate ? frac : 1.0
|
||||
|
||||
// Effective probabilities at this position
|
||||
const pAll = Math.max(0, Math.min(1, pAllStart * difficultyMultiplier))
|
||||
const pAny = Math.max(0, Math.min(1, pAnyStart * difficultyMultiplier))
|
||||
const pOnesOnly = Math.max(0, pAny - pAll)
|
||||
const pNon = Math.max(0, 1 - pAny)
|
||||
|
||||
// Sample category based on probabilities
|
||||
const r = rand()
|
||||
let picked: ProblemCategory
|
||||
if (r < pAll) {
|
||||
picked = 'both'
|
||||
} else if (r < pAll + pOnesOnly) {
|
||||
picked = 'onesOnly'
|
||||
} else {
|
||||
picked = 'non'
|
||||
}
|
||||
|
||||
// Generate problem with retries for uniqueness
|
||||
let tries = 0
|
||||
let ok = false
|
||||
while (tries++ < 3000 && !ok) {
|
||||
let a: number, b: number
|
||||
if (picked === 'both') {
|
||||
;[a, b] = generateBoth(rand)
|
||||
} else if (picked === 'onesOnly') {
|
||||
;[a, b] = generateOnesOnly(rand)
|
||||
} else {
|
||||
;[a, b] = generateNonRegroup(rand)
|
||||
}
|
||||
ok = uniquePush(problems, a, b, seen)
|
||||
|
||||
// If stuck, try a different category
|
||||
if (!ok && tries % 50 === 0) {
|
||||
picked = pick(['both', 'onesOnly', 'non'], rand)
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: add any valid two-digit problem
|
||||
if (!ok) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
uniquePush(problems, a, b, seen)
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Type definitions for double-digit addition worksheet creator
|
||||
|
||||
import type { AdditionConfigV2 } from "../config-schemas";
|
||||
|
||||
/**
|
||||
* Complete, validated configuration for worksheet generation
|
||||
* Extends V2 config with additional derived fields needed for rendering
|
||||
*
|
||||
* Note: Includes V1 compatibility fields during migration period
|
||||
*/
|
||||
export type WorksheetConfig = AdditionConfigV2 & {
|
||||
// Problem set - DERIVED state
|
||||
total: number; // total = problemsPerPage * pages
|
||||
rows: number; // rows = (problemsPerPage / cols) * pages
|
||||
|
||||
// Personalization
|
||||
date: string;
|
||||
seed: number;
|
||||
|
||||
// Layout
|
||||
page: {
|
||||
wIn: number;
|
||||
hIn: number;
|
||||
};
|
||||
margins: {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
};
|
||||
|
||||
// V1 compatibility: Include individual boolean flags during migration
|
||||
// These will be derived from displayRules during validation
|
||||
showCarryBoxes: boolean;
|
||||
showAnswerBoxes: boolean;
|
||||
showPlaceValueColors: boolean;
|
||||
showProblemNumbers: boolean;
|
||||
showCellBorder: boolean;
|
||||
showTenFrames: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partial form state - user may be editing, fields optional
|
||||
* Based on V2 config with additional derived state
|
||||
*
|
||||
* Note: For backwards compatibility during migration, this type accepts either:
|
||||
* - V2 displayRules (preferred)
|
||||
* - V1 individual boolean flags (will be converted to displayRules)
|
||||
*/
|
||||
export type WorksheetFormState = Partial<Omit<AdditionConfigV2, "version">> & {
|
||||
// DERIVED state (calculated from primary state)
|
||||
rows?: number;
|
||||
total?: number;
|
||||
date?: string;
|
||||
seed?: number;
|
||||
|
||||
// V1 compatibility: Accept individual boolean flags
|
||||
// These will be converted to displayRules internally
|
||||
showCarryBoxes?: boolean;
|
||||
showAnswerBoxes?: boolean;
|
||||
showPlaceValueColors?: boolean;
|
||||
showProblemNumbers?: boolean;
|
||||
showCellBorder?: boolean;
|
||||
showTenFrames?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single addition problem
|
||||
*/
|
||||
export interface AdditionProblem {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation result
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
config?: WorksheetConfig;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Problem category for difficulty control
|
||||
*/
|
||||
export type ProblemCategory = "non" | "onesOnly" | "both";
|
||||
@@ -1,147 +0,0 @@
|
||||
// Typst document generator for addition worksheets
|
||||
|
||||
import type { AdditionProblem, WorksheetConfig } from './types'
|
||||
import { generateTypstHelpers, generateProblemStackFunction } from './typstHelpers'
|
||||
|
||||
/**
|
||||
* Chunk array into pages of specified size
|
||||
*/
|
||||
function chunkProblems(problems: AdditionProblem[], pageSize: number): AdditionProblem[][] {
|
||||
const pages: AdditionProblem[][] = []
|
||||
for (let i = 0; i < problems.length; i += pageSize) {
|
||||
pages.push(problems.slice(i, i + pageSize))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Typst source code for a single page
|
||||
*/
|
||||
function generatePageTypst(
|
||||
config: WorksheetConfig,
|
||||
pageProblems: AdditionProblem[],
|
||||
problemOffset: number,
|
||||
rowsPerPage: number
|
||||
): string {
|
||||
const problemsTypst = pageProblems.map((p) => ` (a: ${p.a}, b: ${p.b}),`).join('\n')
|
||||
|
||||
// Calculate actual number of rows on this page
|
||||
const actualRows = Math.ceil(pageProblems.length / config.cols)
|
||||
|
||||
// Use smaller margins to maximize space
|
||||
const margin = 0.4
|
||||
const contentWidth = config.page.wIn - margin * 2
|
||||
const contentHeight = config.page.hIn - margin * 2
|
||||
|
||||
// Calculate grid spacing based on ACTUAL rows on this page
|
||||
const headerHeight = 0.35 // inches for header
|
||||
const availableHeight = contentHeight - headerHeight
|
||||
const problemBoxHeight = availableHeight / actualRows
|
||||
const problemBoxWidth = contentWidth / config.cols
|
||||
|
||||
// Calculate cell size to fill the entire problem box
|
||||
// Without ten-frames: 5 rows (carry, first number, second number, line, answer)
|
||||
// With ten-frames: 5 rows + ten-frames row (0.8 * cellSize for square cells)
|
||||
// Total with ten-frames: 5.8 rows, reduced breathing room to maximize size
|
||||
const cellSize = config.showTenFrames ? problemBoxHeight / 6.0 : problemBoxHeight / 4.5
|
||||
|
||||
return String.raw`
|
||||
// addition-worksheet-page.typ (auto-generated)
|
||||
|
||||
#set page(
|
||||
width: ${config.page.wIn}in,
|
||||
height: ${config.page.hIn}in,
|
||||
margin: ${margin}in,
|
||||
fill: white
|
||||
)
|
||||
#set text(size: ${config.fontSize}pt, font: "New Computer Modern Math")
|
||||
|
||||
// Single non-breakable block to ensure one page
|
||||
#block(breakable: false)[
|
||||
|
||||
#let grid-stroke = ${config.showCellBorder ? '(thickness: 1pt, dash: "dashed", paint: gray.darken(20%))' : 'none'}
|
||||
#let heavy-stroke = 0.8pt
|
||||
#let show-carries = ${config.showCarryBoxes ? 'true' : 'false'}
|
||||
#let show-answers = ${config.showAnswerBoxes ? 'true' : 'false'}
|
||||
#let show-colors = ${config.showPlaceValueColors ? 'true' : 'false'}
|
||||
#let show-numbers = ${config.showProblemNumbers ? 'true' : 'false'}
|
||||
#let show-ten-frames = ${config.showTenFrames ? 'true' : 'false'}
|
||||
#let show-ten-frames-for-all = ${config.showTenFramesForAll ? 'true' : 'false'}
|
||||
|
||||
${generateTypstHelpers(cellSize)}
|
||||
|
||||
${generateProblemStackFunction(cellSize)}
|
||||
|
||||
#let problem-box(problem, index) = {
|
||||
let a = problem.a
|
||||
let b = problem.b
|
||||
let aT = calc.floor(calc.rem(a, 100) / 10)
|
||||
let aO = calc.rem(a, 10)
|
||||
let bT = calc.floor(calc.rem(b, 100) / 10)
|
||||
let bO = calc.rem(b, 10)
|
||||
|
||||
box(
|
||||
inset: 0pt,
|
||||
width: ${problemBoxWidth}in,
|
||||
height: ${problemBoxHeight}in
|
||||
)[
|
||||
#align(center + horizon)[
|
||||
#problem-stack(a, b, aT, aO, bT, bO, index)
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
#let problems = (
|
||||
${problemsTypst}
|
||||
)
|
||||
|
||||
// Compact header - name on left, date on right
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
align: (left, right),
|
||||
text(size: 0.75em, weight: "bold")[${config.name}],
|
||||
text(size: 0.65em)[${config.date}]
|
||||
)
|
||||
#v(${headerHeight}in - 0.25in)
|
||||
|
||||
// Problem grid - exactly ${actualRows} rows × ${config.cols} columns
|
||||
#grid(
|
||||
columns: ${config.cols},
|
||||
column-gutter: 0pt,
|
||||
row-gutter: 0pt,
|
||||
stroke: grid-stroke,
|
||||
..for r in range(0, ${actualRows}) {
|
||||
for c in range(0, ${config.cols}) {
|
||||
let idx = r * ${config.cols} + c
|
||||
if idx < problems.len() {
|
||||
(problem-box(problems.at(idx), ${problemOffset} + idx),)
|
||||
} else {
|
||||
(box(width: ${problemBoxWidth}in, height: ${problemBoxHeight}in),)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
] // End of constrained block
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Typst source code for the worksheet (returns array of page sources)
|
||||
*/
|
||||
export function generateTypstSource(
|
||||
config: WorksheetConfig,
|
||||
problems: AdditionProblem[]
|
||||
): string[] {
|
||||
// Use the problemsPerPage directly from config (primary state)
|
||||
const problemsPerPage = config.problemsPerPage
|
||||
const rowsPerPage = problemsPerPage / config.cols
|
||||
|
||||
// Chunk problems into discrete pages
|
||||
const pages = chunkProblems(problems, problemsPerPage)
|
||||
|
||||
// Generate separate Typst source for each page
|
||||
return pages.map((pageProblems, pageIndex) =>
|
||||
generatePageTypst(config, pageProblems, pageIndex * problemsPerPage, rowsPerPage)
|
||||
)
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// Shared Typst helper functions and components for addition worksheets
|
||||
// Used by both full worksheets and compact examples
|
||||
|
||||
export interface DisplayOptions {
|
||||
showCarryBoxes: boolean
|
||||
showAnswerBoxes: boolean
|
||||
showPlaceValueColors: boolean
|
||||
showProblemNumbers: boolean
|
||||
showCellBorder: boolean
|
||||
showTenFrames: boolean
|
||||
showTenFramesForAll: boolean
|
||||
fontSize: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Typst helper functions (ten-frames, diagonal boxes, etc.)
|
||||
* These are shared between full worksheets and examples
|
||||
*/
|
||||
export function generateTypstHelpers(cellSize: number): string {
|
||||
return String.raw`
|
||||
// Place value colors (light pastels)
|
||||
#let color-ones = rgb(227, 242, 253) // Light blue
|
||||
#let color-tens = rgb(232, 245, 233) // Light green
|
||||
#let color-hundreds = rgb(255, 249, 196) // Light yellow
|
||||
#let color-none = white // No color
|
||||
|
||||
// Ten-frame helper - stacked 2 frames vertically, sized to fit cell width
|
||||
#let ten-frame-spacing = 0pt
|
||||
#let ten-frame-cell-stroke = 0.4pt
|
||||
#let ten-frame-cell-color = rgb(0, 0, 0, 30%)
|
||||
#let ten-frame-outer-stroke = 0.8pt
|
||||
#let ten-frames-stacked(cell-width, top-color, bottom-color) = {
|
||||
let cell-w = cell-width / 5
|
||||
let cell-h = cell-w // Square cells
|
||||
stack(
|
||||
dir: ttb,
|
||||
spacing: ten-frame-spacing,
|
||||
// Top ten-frame (carry to next place value)
|
||||
box(stroke: ten-frame-outer-stroke + black, inset: 0pt)[
|
||||
#grid(
|
||||
columns: 5, rows: 2, gutter: 0pt, stroke: none,
|
||||
..for i in range(0, 10) {
|
||||
(box(width: cell-w, height: cell-h, fill: top-color, stroke: ten-frame-cell-stroke + ten-frame-cell-color)[],)
|
||||
}
|
||||
)
|
||||
],
|
||||
// Bottom ten-frame (current place value overflow)
|
||||
box(stroke: ten-frame-outer-stroke + black, inset: 0pt)[
|
||||
#grid(
|
||||
columns: 5, rows: 2, gutter: 0pt, stroke: none,
|
||||
..for i in range(0, 10) {
|
||||
(box(width: cell-w, height: cell-h, fill: bottom-color, stroke: ten-frame-cell-stroke + ten-frame-cell-color)[],)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// Diagonal-split box for carry cells
|
||||
// Shows the transition from one place value to another
|
||||
// source-color: color of the place value where the carry comes FROM (right side)
|
||||
// dest-color: color of the place value where the carry goes TO (left side)
|
||||
#let diagonal-split-box(cell-size, source-color, dest-color) = {
|
||||
box(width: cell-size, height: cell-size, stroke: 0.5pt)[
|
||||
// Bottom-right triangle (source place value)
|
||||
#place(
|
||||
bottom + right,
|
||||
polygon(
|
||||
fill: source-color,
|
||||
stroke: none,
|
||||
(0pt, 0pt), // bottom-left corner of triangle
|
||||
(cell-size, 0pt), // bottom-right corner
|
||||
(cell-size, cell-size) // top-right corner
|
||||
)
|
||||
)
|
||||
// Top-left triangle (destination place value)
|
||||
#place(
|
||||
top + left,
|
||||
polygon(
|
||||
fill: dest-color,
|
||||
stroke: none,
|
||||
(0pt, 0pt), // top-left corner
|
||||
(cell-size, cell-size), // bottom-right corner of triangle
|
||||
(0pt, cell-size) // bottom-left corner
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Typst function for rendering problem stack/grid
|
||||
* This is the SINGLE SOURCE OF TRUTH for problem rendering layout
|
||||
* Used by both full worksheets and preview examples
|
||||
*/
|
||||
export function generateProblemStackFunction(cellSize: number): string {
|
||||
const cellSizeIn = `${cellSize}in`
|
||||
const cellSizePt = cellSize * 72
|
||||
|
||||
return String.raw`
|
||||
// Problem rendering function for addition worksheets
|
||||
// Returns the stack/grid structure for rendering a single 2-digit addition problem
|
||||
#let problem-stack(a, b, aT, aO, bT, bO, index-or-none) = {
|
||||
stack(
|
||||
dir: ttb,
|
||||
spacing: 0pt,
|
||||
if show-numbers and index-or-none != none {
|
||||
align(top + left)[
|
||||
#box(inset: (left: 0.08in, top: 0.05in))[
|
||||
#text(size: ${(cellSizePt * 0.6).toFixed(1)}pt, weight: "bold", font: "New Computer Modern Math")[\##(index-or-none + 1).]
|
||||
]
|
||||
]
|
||||
},
|
||||
grid(
|
||||
columns: (0.5em, ${cellSizeIn}, ${cellSizeIn}, ${cellSizeIn}),
|
||||
gutter: 0pt,
|
||||
|
||||
[],
|
||||
// Hundreds carry box: shows carry FROM tens (green) TO hundreds (yellow)
|
||||
if show-carries {
|
||||
if show-colors {
|
||||
diagonal-split-box(${cellSizeIn}, color-tens, color-hundreds)
|
||||
} else {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[]
|
||||
}
|
||||
} else { v(${cellSizeIn}) },
|
||||
// Tens carry box: shows carry FROM ones (blue) TO tens (green)
|
||||
if show-carries {
|
||||
if show-colors {
|
||||
diagonal-split-box(${cellSizeIn}, color-ones, color-tens)
|
||||
} else {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[]
|
||||
}
|
||||
} else { v(${cellSizeIn}) },
|
||||
[],
|
||||
|
||||
[],
|
||||
[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-tens } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(aT)]]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-ones } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(aO)]]],
|
||||
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn})[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[+]]],
|
||||
[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-tens } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(bT)]]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-ones } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(bO)]]],
|
||||
|
||||
// Line row
|
||||
[],
|
||||
line(length: ${cellSizeIn}, stroke: heavy-stroke),
|
||||
line(length: ${cellSizeIn}, stroke: heavy-stroke),
|
||||
line(length: ${cellSizeIn}, stroke: heavy-stroke),
|
||||
|
||||
// Ten-frames row with overlaid line on top
|
||||
..if show-ten-frames {
|
||||
let carry = if (aO + bO) >= 10 { 1 } else { 0 }
|
||||
let tens-regroup = (aT + bT + carry) >= 10
|
||||
let ones-regroup = (aO + bO) >= 10
|
||||
let needs-ten-frames = show-ten-frames-for-all or tens-regroup or ones-regroup
|
||||
|
||||
if needs-ten-frames {
|
||||
(
|
||||
[],
|
||||
[], // Empty cell for hundreds column
|
||||
if show-ten-frames-for-all or tens-regroup {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
|
||||
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { color-hundreds } else { color-none }, if show-colors { color-tens } else { color-none })]
|
||||
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
|
||||
]
|
||||
h(2.5pt)
|
||||
} else {
|
||||
v(${cellSizeIn} * 0.8)
|
||||
},
|
||||
if show-ten-frames-for-all or ones-regroup {
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
|
||||
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { color-tens } else { color-none }, if show-colors { color-ones } else { color-none })]
|
||||
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
|
||||
]
|
||||
} else {
|
||||
v(${cellSizeIn} * 0.8)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
()
|
||||
}
|
||||
} else {
|
||||
()
|
||||
},
|
||||
|
||||
// Answer boxes
|
||||
[],
|
||||
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-hundreds } else { color-none })[] } else { v(${cellSizeIn}) },
|
||||
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-tens } else { color-none })[] } else { v(${cellSizeIn}) },
|
||||
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-ones } else { color-none })[] } else { v(${cellSizeIn}) },
|
||||
)
|
||||
)
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED: Old generateProblemTypst function - use generateProblemStackFunction() instead
|
||||
* This function is kept for backwards compatibility but should not be used
|
||||
* Generate Typst code for rendering a single addition problem
|
||||
* This is the core rendering logic shared between worksheets and examples
|
||||
*/
|
||||
export function generateProblemTypst(
|
||||
addend1: number,
|
||||
addend2: number,
|
||||
cellSize: number,
|
||||
options: DisplayOptions,
|
||||
problemNumber?: number
|
||||
): string {
|
||||
const cellSizeIn = `${cellSize}in`
|
||||
const cellSizePt = cellSize * 72
|
||||
|
||||
return String.raw`
|
||||
#let a = ${addend1}
|
||||
#let b = ${addend2}
|
||||
#let aH = calc.floor(a / 100)
|
||||
#let aT = calc.floor(calc.rem(a, 100) / 10)
|
||||
#let aO = calc.rem(a, 10)
|
||||
#let bH = calc.floor(b / 100)
|
||||
#let bT = calc.floor(calc.rem(b, 100) / 10)
|
||||
#let bO = calc.rem(b, 10)
|
||||
|
||||
#stack(
|
||||
dir: ttb,
|
||||
spacing: 0pt,
|
||||
${
|
||||
options.showProblemNumbers && problemNumber !== undefined
|
||||
? `align(top + left)[
|
||||
#box(inset: (left: 0.08in, top: 0.05in))[
|
||||
#text(size: ${(cellSizePt * 0.6).toFixed(1)}pt, weight: "bold", font: "New Computer Modern Math")[\\#${problemNumber}.]
|
||||
]
|
||||
],`
|
||||
: ''
|
||||
}
|
||||
grid(
|
||||
columns: (0.5em, ${cellSizeIn}, ${cellSizeIn}, ${cellSizeIn}),
|
||||
gutter: 0pt,
|
||||
|
||||
[],
|
||||
// Hundreds carry box: shows carry FROM tens (green) TO hundreds (yellow)
|
||||
${
|
||||
options.showCarryBoxes
|
||||
? options.showPlaceValueColors
|
||||
? 'diagonal-split-box(' + cellSizeIn + ', color-tens, color-hundreds),'
|
||||
: 'box(width: ' + cellSizeIn + ', height: ' + cellSizeIn + ', stroke: 0.5pt)[],'
|
||||
: 'v(' + cellSizeIn + '),'
|
||||
}
|
||||
// Tens carry box: shows carry FROM ones (blue) TO tens (green)
|
||||
${
|
||||
options.showCarryBoxes
|
||||
? options.showPlaceValueColors
|
||||
? 'diagonal-split-box(' + cellSizeIn + ', color-ones, color-tens),'
|
||||
: 'box(width: ' + cellSizeIn + ', height: ' + cellSizeIn + ', stroke: 0.5pt)[],'
|
||||
: 'v(' + cellSizeIn + '),'
|
||||
}
|
||||
[],
|
||||
|
||||
// First addend
|
||||
[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-hundreds' : 'color-none'})[#align(center + horizon)[#if aH > 0 [#aH] else [#h(0pt)]]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-tens' : 'color-none'})[#align(center + horizon)[#aT]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-ones' : 'color-none'})[#align(center + horizon)[#aO]],
|
||||
|
||||
// Second addend with + sign
|
||||
[+],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-hundreds' : 'color-none'})[#align(center + horizon)[#if bH > 0 [#bH] else [#h(0pt)]]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-tens' : 'color-none'})[#align(center + horizon)[#bT]],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-ones' : 'color-none'})[#align(center + horizon)[#bO]],
|
||||
|
||||
// Horizontal line
|
||||
[],
|
||||
box(width: ${cellSizeIn}, height: 1pt, inset: 0pt)[#line(length: 100%, stroke: 0.8pt)],
|
||||
box(width: ${cellSizeIn}, height: 1pt, inset: 0pt)[#line(length: 100%, stroke: 0.8pt)],
|
||||
box(width: ${cellSizeIn}, height: 1pt, inset: 0pt)[#line(length: 100%, stroke: 0.8pt)],
|
||||
|
||||
// Answer boxes (or blank space)
|
||||
${
|
||||
options.showAnswerBoxes
|
||||
? `[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: color-none, stroke: grid-stroke, inset: 0pt)[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: color-none, stroke: grid-stroke, inset: 0pt)[],
|
||||
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: color-none, stroke: grid-stroke, inset: 0pt)[],`
|
||||
: ''
|
||||
}
|
||||
)${
|
||||
options.showTenFrames || options.showTenFramesForAll
|
||||
? `,
|
||||
v(4pt),
|
||||
box(inset: 2pt)[
|
||||
#ten-frames-stacked(${cellSizeIn}, color-ones, color-tens)
|
||||
]`
|
||||
: ''
|
||||
}
|
||||
)
|
||||
`
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Validation logic for worksheet configuration
|
||||
|
||||
import type { WorksheetFormState, WorksheetConfig, ValidationResult } from './types'
|
||||
|
||||
/**
|
||||
* Get current date formatted as "Month Day, Year"
|
||||
*/
|
||||
function getDefaultDate(): string {
|
||||
const now = new Date()
|
||||
return now.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and create complete config from partial form state
|
||||
*/
|
||||
export function validateWorksheetConfig(formState: WorksheetFormState): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate total (must be positive, reasonable limit)
|
||||
const total = formState.total ?? 20
|
||||
if (total < 1 || total > 100) {
|
||||
errors.push('Total problems must be between 1 and 100')
|
||||
}
|
||||
|
||||
// Validate cols and auto-calculate rows
|
||||
const cols = formState.cols ?? 4
|
||||
if (cols < 1 || cols > 10) {
|
||||
errors.push('Columns must be between 1 and 10')
|
||||
}
|
||||
|
||||
// Auto-calculate rows to fit all problems
|
||||
const rows = Math.ceil(total / cols)
|
||||
|
||||
// Validate probabilities (0-1 range)
|
||||
const pAnyStart = formState.pAnyStart ?? 0.75
|
||||
const pAllStart = formState.pAllStart ?? 0.25
|
||||
if (pAnyStart < 0 || pAnyStart > 1) {
|
||||
errors.push('pAnyStart must be between 0 and 1')
|
||||
}
|
||||
if (pAllStart < 0 || pAllStart > 1) {
|
||||
errors.push('pAllStart must be between 0 and 1')
|
||||
}
|
||||
if (pAllStart > pAnyStart) {
|
||||
errors.push('pAllStart cannot be greater than pAnyStart')
|
||||
}
|
||||
|
||||
// Validate fontSize
|
||||
const fontSize = formState.fontSize ?? 16
|
||||
if (fontSize < 8 || fontSize > 32) {
|
||||
errors.push('Font size must be between 8 and 32')
|
||||
}
|
||||
|
||||
// Validate seed (must be positive integer)
|
||||
const seed = formState.seed ?? Date.now() % 2147483647
|
||||
if (!Number.isInteger(seed) || seed < 0) {
|
||||
errors.push('Seed must be a non-negative integer')
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { isValid: false, errors }
|
||||
}
|
||||
|
||||
// Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols)
|
||||
const orientation = formState.orientation || (cols <= 3 ? 'portrait' : 'landscape')
|
||||
|
||||
// Get primary state values
|
||||
const problemsPerPage = formState.problemsPerPage ?? total
|
||||
const pages = formState.pages ?? 1
|
||||
|
||||
// Build complete config with defaults
|
||||
const config: WorksheetConfig = {
|
||||
// Primary state
|
||||
problemsPerPage,
|
||||
cols,
|
||||
pages,
|
||||
// Derived state
|
||||
total,
|
||||
rows,
|
||||
// Other fields
|
||||
name: formState.name?.trim() || 'Student',
|
||||
date: formState.date?.trim() || getDefaultDate(),
|
||||
pAnyStart,
|
||||
pAllStart,
|
||||
interpolate: formState.interpolate ?? true,
|
||||
page: {
|
||||
wIn: orientation === 'portrait' ? 8.5 : 11,
|
||||
hIn: orientation === 'portrait' ? 11 : 8.5,
|
||||
},
|
||||
margins: {
|
||||
left: 0.6,
|
||||
right: 0.6,
|
||||
top: 1.1,
|
||||
bottom: 0.7,
|
||||
},
|
||||
showCarryBoxes: formState.showCarryBoxes ?? true,
|
||||
showAnswerBoxes: formState.showAnswerBoxes ?? true,
|
||||
showPlaceValueColors: formState.showPlaceValueColors ?? true,
|
||||
showProblemNumbers: formState.showProblemNumbers ?? true,
|
||||
showCellBorder: formState.showCellBorder ?? true,
|
||||
showTenFrames: formState.showTenFrames ?? false,
|
||||
showTenFramesForAll: formState.showTenFramesForAll ?? false,
|
||||
fontSize,
|
||||
seed,
|
||||
}
|
||||
|
||||
return { isValid: true, config }
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Versioned worksheet config schemas with type-safe validation and migration
|
||||
*
|
||||
* ADDING NEW VERSIONS:
|
||||
* 1. Create new schema (e.g., additionConfigV2Schema)
|
||||
* 2. Add migration function (e.g., migrateAdditionV1toV2)
|
||||
* 3. Update CURRENT_VERSION constant
|
||||
* 4. Add case to migrateAdditionConfig()
|
||||
*
|
||||
* ADDING NEW WORKSHEET TYPES:
|
||||
* 1. Create schema with version field
|
||||
* 2. Create migration function
|
||||
* 3. Export parseXXXConfig() helper
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ADDITION WORKSHEETS
|
||||
// =============================================================================
|
||||
|
||||
/** Current schema version for addition worksheets */
|
||||
const ADDITION_CURRENT_VERSION = 1
|
||||
|
||||
/**
|
||||
* Addition worksheet config - Version 1
|
||||
* Initial schema with ten-frames support
|
||||
*/
|
||||
export const additionConfigV1Schema = z.object({
|
||||
version: z.literal(1),
|
||||
problemsPerPage: z.number().int().min(1).max(100),
|
||||
cols: z.number().int().min(1).max(10),
|
||||
pages: z.number().int().min(1).max(20),
|
||||
orientation: z.enum(['portrait', 'landscape']),
|
||||
name: z.string(),
|
||||
pAnyStart: z.number().min(0).max(1),
|
||||
pAllStart: z.number().min(0).max(1),
|
||||
interpolate: z.boolean(),
|
||||
showCarryBoxes: z.boolean(),
|
||||
showAnswerBoxes: z.boolean(),
|
||||
showPlaceValueColors: z.boolean(),
|
||||
showProblemNumbers: z.boolean(),
|
||||
showCellBorder: z.boolean(),
|
||||
showTenFrames: z.boolean(),
|
||||
showTenFramesForAll: z.boolean(),
|
||||
fontSize: z.number().int().min(8).max(32),
|
||||
})
|
||||
|
||||
export type AdditionConfigV1 = z.infer<typeof additionConfigV1Schema>
|
||||
|
||||
/** Union of all addition config versions (add new versions here) */
|
||||
export const additionConfigSchema = z.discriminatedUnion('version', [
|
||||
additionConfigV1Schema,
|
||||
// additionConfigV2Schema, // Future versions go here
|
||||
])
|
||||
|
||||
export type AdditionConfig = z.infer<typeof additionConfigSchema>
|
||||
|
||||
/**
|
||||
* Default addition config (always latest version)
|
||||
*/
|
||||
export const defaultAdditionConfig: AdditionConfigV1 = {
|
||||
version: 1,
|
||||
problemsPerPage: 20,
|
||||
cols: 5,
|
||||
pages: 1,
|
||||
orientation: 'landscape',
|
||||
name: '',
|
||||
pAnyStart: 0.75,
|
||||
pAllStart: 0.25,
|
||||
interpolate: true,
|
||||
showCarryBoxes: true,
|
||||
showAnswerBoxes: true,
|
||||
showPlaceValueColors: true,
|
||||
showProblemNumbers: true,
|
||||
showCellBorder: true,
|
||||
showTenFrames: false,
|
||||
showTenFramesForAll: false,
|
||||
fontSize: 16,
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate addition config from any version to latest
|
||||
* @throws {Error} if config is invalid or migration fails
|
||||
*/
|
||||
export function migrateAdditionConfig(rawConfig: unknown): AdditionConfigV1 {
|
||||
// First, try to parse as any known version
|
||||
const parsed = additionConfigSchema.safeParse(rawConfig)
|
||||
|
||||
if (!parsed.success) {
|
||||
// If parsing fails completely, return defaults
|
||||
console.warn('Failed to parse addition config, using defaults:', parsed.error)
|
||||
return defaultAdditionConfig
|
||||
}
|
||||
|
||||
const config = parsed.data
|
||||
|
||||
// Migrate to latest version
|
||||
switch (config.version) {
|
||||
case 1:
|
||||
// Already latest version
|
||||
return config
|
||||
|
||||
// Future migrations:
|
||||
// case 2:
|
||||
// return migrateAdditionV2toV3(config)
|
||||
|
||||
default:
|
||||
// Unknown version, return defaults
|
||||
console.warn(`Unknown addition config version: ${(config as any).version}`)
|
||||
return defaultAdditionConfig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate addition config from JSON string
|
||||
* Automatically migrates old versions to latest
|
||||
*/
|
||||
export function parseAdditionConfig(jsonString: string): AdditionConfigV1 {
|
||||
try {
|
||||
const raw = JSON.parse(jsonString)
|
||||
return migrateAdditionConfig(raw)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse addition config JSON:', error)
|
||||
return defaultAdditionConfig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize addition config to JSON string
|
||||
* Ensures version field is set to current version
|
||||
*/
|
||||
export function serializeAdditionConfig(config: Omit<AdditionConfigV1, 'version'>): string {
|
||||
const versioned: AdditionConfigV1 = {
|
||||
...config,
|
||||
version: ADDITION_CURRENT_VERSION,
|
||||
}
|
||||
return JSON.stringify(versioned)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FUTURE WORKSHEET TYPES (subtraction, multiplication, etc.)
|
||||
// =============================================================================
|
||||
|
||||
// Example structure for future worksheet types:
|
||||
//
|
||||
// export const subtractionConfigV1Schema = z.object({
|
||||
// version: z.literal(1),
|
||||
// // ... fields specific to subtraction worksheets
|
||||
// })
|
||||
//
|
||||
// export function parseSubtractionConfig(jsonString: string): SubtractionConfigV1 {
|
||||
// // ... similar to parseAdditionConfig
|
||||
// }
|
||||
@@ -3,18 +3,6 @@
|
||||
/* Import Panda CSS generated styles */
|
||||
@import "../../styled-system/styles.css";
|
||||
|
||||
/* Layout variables */
|
||||
:root {
|
||||
/* Navigation bar heights - used by both the nav itself and content padding */
|
||||
--app-nav-height-full: 72px;
|
||||
--app-nav-height-minimal: 92px;
|
||||
}
|
||||
|
||||
/* Utility class for pages with fixed nav */
|
||||
.with-fixed-nav {
|
||||
padding-top: var(--app-nav-height, 80px);
|
||||
}
|
||||
|
||||
/* Custom global styles */
|
||||
body {
|
||||
font-family:
|
||||
@@ -57,13 +45,3 @@ body {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import Link from 'next/link'
|
||||
import { useMessages, useTranslations } from 'next-intl'
|
||||
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
|
||||
import { getTutorialForEditor } from '@/utils/tutorialConverter'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
@@ -10,8 +9,6 @@ import { grid } from '../../../../styled-system/patterns'
|
||||
|
||||
export function ArithmeticOperationsGuide() {
|
||||
const appConfig = useAbacusConfig()
|
||||
const messages = useMessages() as any
|
||||
const t = useTranslations('guide.arithmetic')
|
||||
|
||||
return (
|
||||
<div className={css({ maxW: '4xl', mx: 'auto' })}>
|
||||
@@ -34,7 +31,7 @@ export function ArithmeticOperationsGuide() {
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
{t('title')}
|
||||
🧮 Arithmetic Operations
|
||||
</h2>
|
||||
<p
|
||||
className={css({
|
||||
@@ -42,7 +39,7 @@ export function ArithmeticOperationsGuide() {
|
||||
opacity: '0.9',
|
||||
})}
|
||||
>
|
||||
{t('subtitle')}
|
||||
Master addition, subtraction, multiplication, and division on the soroban
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -69,10 +66,13 @@ export function ArithmeticOperationsGuide() {
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
{t('addition.title')}
|
||||
<span>➕</span> Addition
|
||||
</h3>
|
||||
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>{t('addition.description')}</p>
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>
|
||||
Addition on the soroban follows the principle of moving beads toward the bar to increase
|
||||
values.
|
||||
</p>
|
||||
|
||||
<div className={css({ mb: '6' })}>
|
||||
<h4
|
||||
@@ -83,7 +83,7 @@ export function ArithmeticOperationsGuide() {
|
||||
color: 'green.600',
|
||||
})}
|
||||
>
|
||||
{t('addition.basicSteps.title')}
|
||||
Basic Steps:
|
||||
</h4>
|
||||
<ol
|
||||
className={css({
|
||||
@@ -92,11 +92,12 @@ export function ArithmeticOperationsGuide() {
|
||||
color: 'gray.700',
|
||||
})}
|
||||
>
|
||||
{(t.raw('addition.basicSteps.steps') as string[]).map((step, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
{i + 1}. {step}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>1. Set the first number on the soroban</li>
|
||||
<li className={css({ mb: '2' })}>
|
||||
2. Add the second number by moving beads toward the bar
|
||||
</li>
|
||||
<li className={css({ mb: '2' })}>3. Handle carries when a column exceeds 9</li>
|
||||
<li>4. Read the final result</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -117,13 +118,11 @@ export function ArithmeticOperationsGuide() {
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
{t('addition.example.title')}
|
||||
Example: 3 + 4 = 7
|
||||
</h5>
|
||||
<div className={grid({ columns: 3, gap: '4', alignItems: 'center' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>
|
||||
{t('addition.example.start')}
|
||||
</p>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>Start: 3</p>
|
||||
<div
|
||||
className={css({
|
||||
width: '160px',
|
||||
@@ -155,9 +154,7 @@ export function ArithmeticOperationsGuide() {
|
||||
</div>
|
||||
<div className={css({ textAlign: 'center', fontSize: '2xl' })}>+</div>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>
|
||||
{t('addition.example.result')}
|
||||
</p>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>Result: 7</p>
|
||||
<div
|
||||
className={css({
|
||||
width: '160px',
|
||||
@@ -214,10 +211,12 @@ export function ArithmeticOperationsGuide() {
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
{t('guidedTutorial.title')}
|
||||
<span>🎯</span> Guided Addition Tutorial
|
||||
</h3>
|
||||
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>{t('guidedTutorial.description')}</p>
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>
|
||||
Learn addition step-by-step with interactive guidance, tooltips, and error correction.
|
||||
</p>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
@@ -240,10 +239,10 @@ export function ArithmeticOperationsGuide() {
|
||||
})}
|
||||
>
|
||||
<span>✏️</span>
|
||||
<strong>{t('guidedTutorial.editableNote')}</strong>
|
||||
<strong>This tutorial is now editable!</strong>
|
||||
</p>
|
||||
<p className={css({ fontSize: 'xs', color: 'blue.600' })}>
|
||||
{t('guidedTutorial.editableDesc')}{' '}
|
||||
You can customize this tutorial using our new tutorial editor system.{' '}
|
||||
<a
|
||||
href="/tutorial-editor"
|
||||
className={css({
|
||||
@@ -252,13 +251,13 @@ export function ArithmeticOperationsGuide() {
|
||||
_hover: { color: 'blue.800' },
|
||||
})}
|
||||
>
|
||||
{t('guidedTutorial.editableLink')}
|
||||
Open in Editor →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TutorialPlayer
|
||||
tutorial={getTutorialForEditor(messages.tutorial || {})}
|
||||
tutorial={getTutorialForEditor()}
|
||||
isDebugMode={false}
|
||||
showDebugPanel={false}
|
||||
/>
|
||||
@@ -287,10 +286,12 @@ export function ArithmeticOperationsGuide() {
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
{t('subtraction.title')}
|
||||
<span>➖</span> Subtraction
|
||||
</h3>
|
||||
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>{t('subtraction.description')}</p>
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>
|
||||
Subtraction involves moving beads away from the bar to decrease values.
|
||||
</p>
|
||||
|
||||
<div className={css({ mb: '6' })}>
|
||||
<h4
|
||||
@@ -301,7 +302,7 @@ export function ArithmeticOperationsGuide() {
|
||||
color: 'red.600',
|
||||
})}
|
||||
>
|
||||
{t('subtraction.basicSteps.title')}
|
||||
Basic Steps:
|
||||
</h4>
|
||||
<ol
|
||||
className={css({
|
||||
@@ -310,11 +311,10 @@ export function ArithmeticOperationsGuide() {
|
||||
color: 'gray.700',
|
||||
})}
|
||||
>
|
||||
{(t.raw('subtraction.basicSteps.steps') as string[]).map((step, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
{i + 1}. {step}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>1. Set the minuend (first number) on the soroban</li>
|
||||
<li className={css({ mb: '2' })}>2. Subtract by moving beads away from the bar</li>
|
||||
<li className={css({ mb: '2' })}>3. Handle borrowing when needed</li>
|
||||
<li>4. Read the final result</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -335,13 +335,11 @@ export function ArithmeticOperationsGuide() {
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
{t('subtraction.example.title')}
|
||||
Example: 8 - 3 = 5
|
||||
</h5>
|
||||
<div className={grid({ columns: 3, gap: '4', alignItems: 'center' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>
|
||||
{t('subtraction.example.start')}
|
||||
</p>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>Start: 8</p>
|
||||
<div
|
||||
className={css({
|
||||
width: '160px',
|
||||
@@ -373,9 +371,7 @@ export function ArithmeticOperationsGuide() {
|
||||
</div>
|
||||
<div className={css({ textAlign: 'center', fontSize: '2xl' })}>-</div>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>
|
||||
{t('subtraction.example.result')}
|
||||
</p>
|
||||
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>Result: 5</p>
|
||||
<div
|
||||
className={css({
|
||||
width: '160px',
|
||||
@@ -432,11 +428,11 @@ export function ArithmeticOperationsGuide() {
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
{t('multiplicationDivision.title')}
|
||||
<span>✖️➗</span> Multiplication & Division
|
||||
</h3>
|
||||
|
||||
<p className={css({ mb: '6', color: 'gray.700' })}>
|
||||
{t('multiplicationDivision.description')}
|
||||
Advanced operations that combine addition/subtraction with position shifting.
|
||||
</p>
|
||||
|
||||
<div className={grid({ columns: { base: 1, md: 2 }, gap: '6' })}>
|
||||
@@ -457,7 +453,7 @@ export function ArithmeticOperationsGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('multiplicationDivision.multiplication.title')}
|
||||
Multiplication
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -466,13 +462,10 @@ export function ArithmeticOperationsGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('multiplicationDivision.multiplication.points') as string[]).map(
|
||||
(point, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<li className={css({ mb: '2' })}>• Break down into repeated addition</li>
|
||||
<li className={css({ mb: '2' })}>• Use position shifts for place values</li>
|
||||
<li className={css({ mb: '2' })}>• Master multiplication tables</li>
|
||||
<li>• Practice with single digits first</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -493,7 +486,7 @@ export function ArithmeticOperationsGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('multiplicationDivision.division.title')}
|
||||
Division
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -502,11 +495,10 @@ export function ArithmeticOperationsGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('multiplicationDivision.division.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>• Use repeated subtraction method</li>
|
||||
<li className={css({ mb: '2' })}>• Estimate quotients carefully</li>
|
||||
<li className={css({ mb: '2' })}>• Handle remainders properly</li>
|
||||
<li>• Check results by multiplication</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -531,7 +523,7 @@ export function ArithmeticOperationsGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('practiceTips.title')}
|
||||
💡 Master the Fundamentals
|
||||
</h4>
|
||||
<p
|
||||
className={css({
|
||||
@@ -539,7 +531,7 @@ export function ArithmeticOperationsGuide() {
|
||||
opacity: '0.9',
|
||||
})}
|
||||
>
|
||||
{t('practiceTips.description')}
|
||||
Start with simple problems and gradually increase complexity
|
||||
</p>
|
||||
<Link
|
||||
href="/create"
|
||||
@@ -556,7 +548,7 @@ export function ArithmeticOperationsGuide() {
|
||||
_hover: { transform: 'translateY(-1px)', shadow: 'lg' },
|
||||
})}
|
||||
>
|
||||
{t('practiceTips.button')}
|
||||
Practice Arithmetic Operations →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { grid, hstack, stack } from '../../../../styled-system/patterns'
|
||||
|
||||
export function ReadingNumbersGuide() {
|
||||
const appConfig = useAbacusConfig()
|
||||
const t = useTranslations('guide.reading')
|
||||
|
||||
return (
|
||||
<div className={stack({ gap: '12' })}>
|
||||
@@ -22,7 +20,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
{t('title')}
|
||||
🔍 Learning to Read Soroban Numbers
|
||||
</h2>
|
||||
<p
|
||||
className={css({
|
||||
@@ -33,7 +31,8 @@ export function ReadingNumbersGuide() {
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
{t('subtitle')}
|
||||
Master the fundamentals of reading numbers on the soroban with step-by-step visual
|
||||
tutorials
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +61,7 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: 'lg',
|
||||
})}
|
||||
>
|
||||
{t('structure.number')}
|
||||
1
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
@@ -71,7 +70,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('structure.title')}
|
||||
Understanding the Structure
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +82,8 @@ export function ReadingNumbersGuide() {
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
{t('structure.description')}
|
||||
The soroban consists of two main sections divided by a horizontal bar. Understanding
|
||||
this structure is fundamental to reading any number.
|
||||
</p>
|
||||
|
||||
<div className={grid({ columns: { base: 1, md: 2 }, gap: '8' })}>
|
||||
@@ -104,7 +104,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('structure.heaven.title')}
|
||||
🌅 Heaven Beads (Top)
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -114,11 +114,10 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('structure.heaven.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>• Located above the horizontal bar</li>
|
||||
<li className={css({ mb: '2' })}>• Each bead represents 5</li>
|
||||
<li className={css({ mb: '2' })}>• Only one bead per column</li>
|
||||
<li>• When pushed down = active/counted</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +138,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('structure.earth.title')}
|
||||
🌍 Earth Beads (Bottom)
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -149,11 +148,10 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('structure.earth.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>• Located below the horizontal bar</li>
|
||||
<li className={css({ mb: '2' })}>• Each bead represents 1</li>
|
||||
<li className={css({ mb: '2' })}>• Four beads per column</li>
|
||||
<li>• When pushed up = active/counted</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,7 +173,7 @@ export function ReadingNumbersGuide() {
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
{t('structure.keyConcept')}
|
||||
💡 Key Concept: Active beads are those touching the horizontal bar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +205,7 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: 'lg',
|
||||
})}
|
||||
>
|
||||
{t('singleDigits.number')}
|
||||
2
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
@@ -216,7 +214,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('singleDigits.title')}
|
||||
Reading Single Digits (1-9)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -227,16 +225,17 @@ export function ReadingNumbersGuide() {
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
{t('singleDigits.description')}
|
||||
Let's learn to read single digits by understanding how heaven and earth beads combine to
|
||||
represent numbers 1 through 9.
|
||||
</p>
|
||||
|
||||
<div className={grid({ columns: { base: 1, lg: 5 }, gap: '6' })}>
|
||||
{[
|
||||
{ num: 0, descKey: '0' },
|
||||
{ num: 1, descKey: '1' },
|
||||
{ num: 3, descKey: '3' },
|
||||
{ num: 5, descKey: '5' },
|
||||
{ num: 7, descKey: '7' },
|
||||
{ num: 0, desc: 'No beads active - all away from bar' },
|
||||
{ num: 1, desc: 'One earth bead pushed up' },
|
||||
{ num: 3, desc: 'Three earth beads pushed up' },
|
||||
{ num: 5, desc: 'Heaven bead pushed down' },
|
||||
{ num: 7, desc: 'Heaven bead + two earth beads' },
|
||||
].map((example) => (
|
||||
<div
|
||||
key={example.num}
|
||||
@@ -245,7 +244,7 @@ export function ReadingNumbersGuide() {
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
rounded: 'lg',
|
||||
p: '2',
|
||||
p: '4',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -257,19 +256,27 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.600',
|
||||
mb: '2',
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{example.num}
|
||||
</div>
|
||||
|
||||
{/* Aspect ratio container for soroban - roughly 1:3 ratio */}
|
||||
<div
|
||||
className={css({
|
||||
flex: '1',
|
||||
width: '100%',
|
||||
aspectRatio: '1/2.8',
|
||||
maxW: '120px',
|
||||
bg: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
mb: '3',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
my: '2',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
<AbacusReact
|
||||
@@ -278,7 +285,7 @@ export function ReadingNumbersGuide() {
|
||||
beadShape={appConfig.beadShape}
|
||||
colorScheme={appConfig.colorScheme}
|
||||
hideInactiveBeads={appConfig.hideInactiveBeads}
|
||||
scaleFactor={1.2}
|
||||
scaleFactor={0.8}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
animated={true}
|
||||
@@ -291,10 +298,10 @@ export function ReadingNumbersGuide() {
|
||||
color: 'gray.600',
|
||||
lineHeight: 'tight',
|
||||
textAlign: 'center',
|
||||
mt: '2',
|
||||
mt: 'auto',
|
||||
})}
|
||||
>
|
||||
{t(`singleDigits.examples.${example.descKey}`)}
|
||||
{example.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -327,7 +334,7 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: 'lg',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.number')}
|
||||
3
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
@@ -336,7 +343,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.title')}
|
||||
Multi-Digit Numbers
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -347,7 +354,8 @@ export function ReadingNumbersGuide() {
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.description')}
|
||||
Reading larger numbers is simply a matter of reading each column from left to right,
|
||||
with each column representing a different place value.
|
||||
</p>
|
||||
|
||||
<div
|
||||
@@ -368,7 +376,7 @@ export function ReadingNumbersGuide() {
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.readingDirection.title')}
|
||||
📍 Reading Direction & Place Values
|
||||
</h4>
|
||||
<div className={grid({ columns: { base: 1, md: 2 }, gap: '6' })}>
|
||||
<div>
|
||||
@@ -379,7 +387,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'purple.800',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.readingDirection.readingOrder.title')}
|
||||
Reading Order:
|
||||
</h5>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -388,13 +396,9 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('multiDigit.readingDirection.readingOrder.points') as string[]).map(
|
||||
(point, i) => (
|
||||
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<li className={css({ mb: '1' })}>• Always read from LEFT to RIGHT</li>
|
||||
<li className={css({ mb: '1' })}>• Each column is one digit</li>
|
||||
<li>• Combine digits to form the complete number</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@@ -405,7 +409,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'purple.800',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.readingDirection.placeValues.title')}
|
||||
Place Values:
|
||||
</h5>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -414,13 +418,9 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('multiDigit.readingDirection.placeValues.points') as string[]).map(
|
||||
(point, i) => (
|
||||
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<li className={css({ mb: '1' })}>• Rightmost = Ones (1s)</li>
|
||||
<li className={css({ mb: '1' })}>• Next left = Tens (10s)</li>
|
||||
<li>• Continue for hundreds, thousands, etc.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,14 +445,20 @@ export function ReadingNumbersGuide() {
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('multiDigit.examples.title')}
|
||||
🔢 Multi-Digit Examples
|
||||
</h4>
|
||||
|
||||
<div className={grid({ columns: { base: 1, md: 3 }, gap: '8' })}>
|
||||
{[
|
||||
{ num: 23, descKey: '23' },
|
||||
{ num: 58, descKey: '58' },
|
||||
{ num: 147, descKey: '147' },
|
||||
{
|
||||
num: 23,
|
||||
desc: 'Two-digit: 2 in tens place + 3 in ones place',
|
||||
},
|
||||
{
|
||||
num: 58,
|
||||
desc: 'Heaven bead in tens (5) + heaven + earth beads in ones (8)',
|
||||
},
|
||||
{ num: 147, desc: 'Three-digit: 1 hundred + 4 tens + 7 ones' },
|
||||
].map((example) => (
|
||||
<div
|
||||
key={example.num}
|
||||
@@ -461,7 +467,7 @@ export function ReadingNumbersGuide() {
|
||||
border: '1px solid',
|
||||
borderColor: 'blue.300',
|
||||
rounded: 'lg',
|
||||
p: '2',
|
||||
p: '4',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -473,19 +479,27 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'blue.600',
|
||||
mb: '2',
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{example.num}
|
||||
</div>
|
||||
|
||||
{/* Larger container for multi-digit numbers */}
|
||||
<div
|
||||
className={css({
|
||||
flex: '1',
|
||||
width: '100%',
|
||||
aspectRatio: '3/4',
|
||||
maxW: '180px',
|
||||
bg: 'gray.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'blue.200',
|
||||
rounded: 'md',
|
||||
mb: '3',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
my: '2',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
<AbacusReact
|
||||
@@ -494,7 +508,7 @@ export function ReadingNumbersGuide() {
|
||||
beadShape={appConfig.beadShape}
|
||||
colorScheme={appConfig.colorScheme}
|
||||
hideInactiveBeads={appConfig.hideInactiveBeads}
|
||||
scaleFactor={1.2}
|
||||
scaleFactor={0.9}
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
animated={true}
|
||||
@@ -507,10 +521,9 @@ export function ReadingNumbersGuide() {
|
||||
color: 'blue.700',
|
||||
lineHeight: 'relaxed',
|
||||
textAlign: 'center',
|
||||
mt: '2',
|
||||
})}
|
||||
>
|
||||
{t(`multiDigit.examples.${example.descKey}`)}
|
||||
{example.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -544,7 +557,7 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: 'lg',
|
||||
})}
|
||||
>
|
||||
{t('practice.number')}
|
||||
4
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
@@ -553,7 +566,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('practice.title')}
|
||||
Practice Strategy
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -575,7 +588,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
{t('practice.learningTips.title')}
|
||||
🎯 Learning Tips
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -585,11 +598,12 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('practice.learningTips.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>• Start with single digits (0-9)</li>
|
||||
<li className={css({ mb: '2' })}>
|
||||
• Practice identifying active vs. inactive beads
|
||||
</li>
|
||||
<li className={css({ mb: '2' })}>• Work on speed recognition</li>
|
||||
<li>• Progress to multi-digit numbers gradually</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -610,7 +624,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
{t('practice.quickRecognition.title')}
|
||||
⚡ Quick Recognition
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -620,11 +634,10 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('practice.quickRecognition.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '2' })}>• Numbers 1-4: Only earth beads</li>
|
||||
<li className={css({ mb: '2' })}>• Number 5: Only heaven bead</li>
|
||||
<li className={css({ mb: '2' })}>• Numbers 6-9: Heaven + earth beads</li>
|
||||
<li>• Zero: All beads away from bar</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -645,7 +658,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('practice.readyToPractice.title')}
|
||||
🚀 Ready to Practice?
|
||||
</h4>
|
||||
<p
|
||||
className={css({
|
||||
@@ -653,7 +666,7 @@ export function ReadingNumbersGuide() {
|
||||
opacity: '0.9',
|
||||
})}
|
||||
>
|
||||
{t('practice.readyToPractice.description')}
|
||||
Test your newfound knowledge with interactive flashcards
|
||||
</p>
|
||||
<Link
|
||||
href="/create"
|
||||
@@ -670,7 +683,7 @@ export function ReadingNumbersGuide() {
|
||||
_hover: { transform: 'translateY(-1px)', shadow: 'lg' },
|
||||
})}
|
||||
>
|
||||
{t('practice.readyToPractice.button')}
|
||||
Create Practice Flashcards →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -701,7 +714,7 @@ export function ReadingNumbersGuide() {
|
||||
fontSize: 'lg',
|
||||
})}
|
||||
>
|
||||
{t('interactive.number')}
|
||||
5
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
@@ -710,7 +723,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('interactive.title')}
|
||||
Interactive Practice
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -721,7 +734,8 @@ export function ReadingNumbersGuide() {
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
{t('interactive.description')}
|
||||
Try the interactive abacus below! Click on the beads to activate them and watch the
|
||||
number change in real-time.
|
||||
</p>
|
||||
|
||||
<div
|
||||
@@ -742,7 +756,7 @@ export function ReadingNumbersGuide() {
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('interactive.howToUse.title')}
|
||||
🎮 How to Use the Interactive Abacus
|
||||
</h4>
|
||||
<div className={grid({ columns: { base: 1, md: 2 }, gap: '6' })}>
|
||||
<div>
|
||||
@@ -753,7 +767,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'orange.800',
|
||||
})}
|
||||
>
|
||||
{t('interactive.howToUse.heaven.title')}
|
||||
Heaven Beads (Top):
|
||||
</h5>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -762,11 +776,9 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('interactive.howToUse.heaven.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '1' })}>• Worth 5 points each</li>
|
||||
<li className={css({ mb: '1' })}>• Click to toggle on/off</li>
|
||||
<li>• Blue when active, gray when inactive</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@@ -777,7 +789,7 @@ export function ReadingNumbersGuide() {
|
||||
color: 'orange.800',
|
||||
})}
|
||||
>
|
||||
{t('interactive.howToUse.earth.title')}
|
||||
Earth Beads (Bottom):
|
||||
</h5>
|
||||
<ul
|
||||
className={css({
|
||||
@@ -786,11 +798,9 @@ export function ReadingNumbersGuide() {
|
||||
pl: '4',
|
||||
})}
|
||||
>
|
||||
{(t.raw('interactive.howToUse.earth.points') as string[]).map((point, i) => (
|
||||
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
|
||||
• {point}
|
||||
</li>
|
||||
))}
|
||||
<li className={css({ mb: '1' })}>• Worth 1 point each</li>
|
||||
<li className={css({ mb: '1' })}>• Click to activate groups</li>
|
||||
<li>• Green when active, gray when inactive</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -839,7 +849,7 @@ export function ReadingNumbersGuide() {
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
{t('interactive.readyToPractice.title')}
|
||||
🚀 Ready to Practice?
|
||||
</h4>
|
||||
<p
|
||||
className={css({
|
||||
@@ -847,7 +857,7 @@ export function ReadingNumbersGuide() {
|
||||
opacity: '0.9',
|
||||
})}
|
||||
>
|
||||
{t('interactive.readyToPractice.description')}
|
||||
Test your newfound knowledge with interactive flashcards
|
||||
</p>
|
||||
<Link
|
||||
href="/create"
|
||||
@@ -864,7 +874,7 @@ export function ReadingNumbersGuide() {
|
||||
_hover: { transform: 'translateY(-1px)', shadow: 'lg' },
|
||||
})}
|
||||
>
|
||||
{t('interactive.readyToPractice.button')}
|
||||
Create Practice Flashcards →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||