feat: functional gridfinity calculator app

This commit is contained in:
w33ble
2026-01-11 19:38:34 -07:00
commit 7c512f5adb
19 changed files with 3561 additions and 0 deletions

190
.gitignore vendored Normal file
View File

@@ -0,0 +1,190 @@
# General
.DS_Store
__MACOSX/
.AppleDouble
.LSOverride
Icon[
]
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Swap
[._]*.s[a-v][a-z]
# comment out the next line if you don't need vector files
!*.svg
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
.output
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/

78
AGENTS.md Normal file
View File

@@ -0,0 +1,78 @@
# Core Workflow & My Behavior
## 1. My Core Interaction Style
* **I do not "yap":** I will be concise and eliminate conversational filler. I focus on technical precision over politeness.
* **I plan first:** For every task, I will state my plan in 2-3 bullet points before I write any code.
* **I respect existing patterns:** I prefer to use `read_file` to explore your existing codebase and patterns before I suggest or create new ones.
## 2. My Engineering Standards
* **I am test-driven:** If the environment allows, I will verify my changes by running a test or starting a dev server. I never assume code works just because it looks correct.
* **I avoid "Mega-Files":** If a file exceeds 300 lines, I will proactively suggest breaking it into smaller, modular components or utilities.
* **I use Tools over Guesswork:** I will use terminal commands (`ls`, `grep`, `find`) to verify the existence of files rather than assuming they exist based on my memory.
## 3. My "Self-Improving" Protocol
* **I evolve my rules:** If I encounter a recurring bug, a tricky error, or a pattern we both agree on, **I MUST** ask: "Should I update the `AGENTS.md` file to remember this pattern?"
* **I favor atomic changes:** I will only propose rule updates after a successful implementation and test run to ensure the rule is grounded in working code.
## 4. Git & Version Control Protocol
- **Atomic Commits:** I will create one commit per logical change. I will never bundle multiple unrelated features into a single commit.
- **Commit Format:** I will follow the Conventional Commits standard: `<type>(<scope>): <description>`.
- **Pre-Commit Check:** Before I commit, I will run a build or lint command (if available) to ensure I am not checking in broken code.
- **Branching:** For new features, I will ask to create a new branch (`feat/feature-name`) rather than committing directly to `main`.
- **Staging:** I prefer to stage files individually.
## 5. My Error Handling
* **I am honest about failures:** If a tool call fails or I am confused by a requirement, I will stop immediately and ask for clarification rather than "hallucinating" a fix.
* **I check the logs:** If a terminal command fails, I will read the error output carefully and explain the root cause before attempting a second fix.
---
# Project Management & Context Rules
## 1. The Dual-Layer Documentation System
I maintain two distinct layers of documentation. I must keep them in sync but never mix their purposes.
### Layer 1: The Blueprint (/docs)
*Role: Permanent reference for humans and AI. The "Source of Truth".*
- **PRD.md**: Core features, target audience, and business logic.
- **Architecture.md**: Tech stack, folder structure, and data flow diagrams.
- **API.md**: Endpoint definitions, request/response schemas.
- **Schema.md**: Database tables, relationships, and types.
### Layer 2: The Memory Bank (/memory-bank)
*Role: Operational state for the AI agent. Updated every session.*
- **projectBrief.md**: The foundation. High-level overview of requirements.
- **productContext.md**: Why the project exists and how it should feel/work.
- **activeContext.md**: **CRITICAL.** Current work focus, recent changes, and active decisions.
- **systemPatterns.md**: Technical decisions and recurring code patterns discovered.
- **progress.md**: Milestone tracking. What is done, in-progress, and pending.
## 2. Required Workflow Protocol
### Phase A: Initialization (New Session)
At the start of every session, I MUST:
1. Read `memory-bank/activeContext.md` and `memory-bank/progress.md` to regain context.
2. Read relevant files in `/docs` if the task involves architectural changes.
3. Verbally confirm the "Current Focus" to the user.
### Phase B: Execution (Act Mode)
- I only perform tasks listed in `memory-bank/progress.md` or as requested by the user.
- If I discover a new pattern or make a technical decision (e.g., "we use absolute imports"), I must immediately update `systemPatterns.md`.
### Phase C: Task Completion
Before calling `attempt_completion`, I MUST:
1. Update `memory-bank/activeContext.md` with a summary of changes.
2. Update `memory-bank/progress.md` by checking off completed items.
3. **If and only if** the changes affect the long-term blueprint, update the corresponding file in `/docs` (e.g., updating `API.md` after adding a route).
## 3. Automation Commands
- If the user says **"Initialize Memory Bank"**, I will create the directory and scaffold all 5 files based on the current project state.
- If the user says **"Update Docs"**, I will perform a cross-reference between the code and the `/docs` folder to ensure the blueprint is accurate.

32
docs/Architecture.md Normal file
View File

@@ -0,0 +1,32 @@
# Architecture - Gridfinity Calculator
## 1. System Design
The application is a client-side only web tool.
```mermaid
graph TD
A[index.html] --> B[main.ts]
B --> C[calculator.ts]
B --> D[DOM Update]
E[User Input] --> B
```
## 2. Components
- **Input Form:** Collects drawer dimensions and unit preferences.
- **Spec Controls:** Accordion or settings panel for overriding Gridfinity defaults.
- **Calculation Engine:** Pure TS module for unit conversion and Gridfinity math.
- **Results Display:** Visual cards/readouts showing calculated values.
## 3. Data Flow
1. User changes an input.
2. `main.ts` catches the event.
3. `main.ts` gathers all current values from the DOM.
4. `main.ts` calls `calculator.ts` functions.
5. `calculator.ts` returns a results object.
6. `main.ts` updates the results section of the DOM.
## 4. Technical Stack
- **Vite:** Asset bundling and dev server.
- **TypeScript:** Type-safe logic.
- **Tailwind CSS:** Utility-first styling for speed and responsiveness.
- **Vitest:** Unit testing logic in isolation.

31
docs/PRD.md Normal file
View File

@@ -0,0 +1,31 @@
# PRD - Gridfinity Calculator
## 1. Overview
The Gridfinity Calculator is a web tool designed to help makers plan their Gridfinity layouts. It calculates the number of baseplates that fit in a specific container and the maximum bin height allowed.
## 2. Target Audience
- 3D printing enthusiasts using the Gridfinity system.
- Workshop organizers and hobbyists.
## 3. Key Features
- **Dimension Input:** Width, Length (Depth), and Height of the target drawer/container.
- **Unit Toggle:** Support for Inches (decimal/fractional) and Millimeters.
- **Spec Overrides:** Modify standard 42mm grid and 7mm height unit.
- **Dynamic Results:**
- Total full grid squares (e.g., "5x4 grids").
- Fit percentage/fractional grids (e.g., "5.2 x 4.1 grids").
- Vertical Unit Height (U) for bins (e.g., "6U").
- Remaining space (gap) in mm/inches.
## 4. Technical Constraints
- Must be fast and lightweight.
- Must work offline once loaded (single-page build).
- Mobile-responsive design.
## 5. Calculation Logic
- `Grid Count = Floor(Drawer Dimension / Grid Spec)`
- `Exact Fit = Drawer Dimension / Grid Spec`
- `Bin Unit Height (U) = Floor((Drawer Height - Base Thickness) / Height Unit)`
- *Default Base Thickness:* 4.8mm
- *Default Grid Spec:* 42mm
- *Default Height Unit:* 7mm

111
index.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gridfinity Calculator</title>
</head>
<body class="bg-slate-50 text-slate-900 min-h-screen">
<div id="app" class="max-w-2xl mx-auto p-4 md:p-8">
<header class="mb-8 text-center">
<h1 class="text-3xl font-bold text-slate-800">Gridfinity Calculator</h1>
<p class="text-slate-600">Plan your modular storage layout</p>
</header>
<main class="grid gap-8">
<!-- Inputs Section -->
<section class="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
<h2 class="text-xl font-semibold mb-4 flex items-center gap-2">
Drawer Dimensions
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Width</label>
<input type="number" id="input-width" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0" step="any" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Length / Depth</label>
<input type="number" id="input-length" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0" step="any" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Height</label>
<input type="number" id="input-height" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0" step="any" />
</div>
</div>
<div class="flex items-center gap-4">
<label class="text-sm font-medium text-slate-700">Units:</label>
<div class="flex bg-slate-100 p-1 rounded-lg">
<button id="unit-mm" class="px-4 py-1 rounded-md text-sm font-medium bg-white shadow-sm">mm</button>
<button id="unit-in" class="px-4 py-1 rounded-md text-sm font-medium text-slate-600 hover:text-slate-900">inches</button>
</div>
</div>
</section>
<!-- Results Section -->
<section id="results" class="bg-blue-600 text-white p-6 rounded-xl shadow-md hidden">
<h2 class="text-xl font-semibold mb-6">Calculation Results</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<div>
<p class="text-blue-100 text-sm uppercase tracking-wider font-semibold">Grid Count</p>
<p class="text-4xl font-bold" id="result-grid-count">0 x 0</p>
<p class="text-blue-100 text-sm mt-1" id="result-grid-fractional">0.00 x 0.00 grids</p>
</div>
<div>
<p class="text-blue-100 text-sm uppercase tracking-wider font-semibold">Max Bin Height</p>
<p class="text-4xl font-bold" id="result-bin-height">0U</p>
<p class="text-blue-100 text-sm mt-1">Vertical Units (7mm each)</p>
</div>
</div>
<div class="bg-blue-700/50 p-4 rounded-lg space-y-3">
<h3 class="font-semibold border-b border-blue-500/50 pb-2">Remaining Space</h3>
<div class="grid grid-cols-2 gap-2 text-sm">
<span>Width Gap:</span>
<span id="gap-width" class="font-mono text-right">0.0 mm</span>
<span>Length Gap:</span>
<span id="gap-length" class="font-mono text-right">0.0 mm</span>
<span>Height Gap:</span>
<span id="gap-height" class="font-mono text-right">0.0 mm</span>
</div>
</div>
</div>
</section>
<!-- Spec Overrides -->
<details class="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<summary class="px-6 py-4 font-medium cursor-pointer hover:bg-slate-50 flex items-center justify-between">
<span>Gridfinity Spec Overrides</span>
<span class="text-slate-400 text-sm font-normal">Standard: 42x42x7mm</span>
</summary>
<div class="p-6 border-t border-slate-100 space-y-4 bg-slate-50/50">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1">Grid Size (mm)</label>
<input type="number" id="spec-grid-size" value="42" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
</div>
<div>
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1">Unit Height (mm)</label>
<input type="number" id="spec-height-unit" value="7" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
</div>
<div>
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1">Base Thickness (mm)</label>
<input type="number" id="spec-base-thickness" value="4.8" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
</div>
</div>
<p class="text-xs text-slate-400 italic">Adjust these to scale the system for an exact fit in your drawer.</p>
</div>
</details>
</main>
<footer class="mt-12 text-center text-slate-400 text-sm">
<p>Follows the Gridfinity spec by Zach Freedman</p>
</footer>
</div>
<script type="module" src="/src/main.ts"></script>
<link rel="stylesheet" href="/src/style.css">
</body>
</html>

View File

@@ -0,0 +1,20 @@
# Active Context
## Current Status
- Initializing the project environment.
- Documentation phase (Memory Bank & Blueprint).
## Recent Changes
- Created `memory-bank/projectBrief.md`.
- Created `memory-bank/productContext.md`.
## Current Focus
- Completing Memory Bank initialization.
- Drafting PRD and Architecture.
- Setting up the Vite/TypeScript/Tailwind environment.
## Active Decisions
- Using **Vite** for the build tool.
- Using **Vanilla TypeScript** to avoid framework overhead.
- Using **Vitest** for unit testing calculation logic.
- Target: A single-page application that can be built into a self-contained output.

View File

@@ -0,0 +1,18 @@
# Product Context
## Why this project exists?
Gridfinity is a popular modular storage system. While the standard is well-defined (42x42x7mm), users often need to:
1. Figure out how many baseplates fit in a drawer.
2. Determine the maximum height of bins for a specific drawer depth.
3. Scale the system slightly to perfectly fill a drawer that doesn't fit a whole number of 42mm grids.
## User Experience
- **Input-First:** Users should immediately see where to enter their dimensions.
- **Instant Feedback:** Results should update as they type.
- **Flexibility:** Toggle between mm and inches easily.
- **Precision:** Clear output showing "Full Grids" and "Vertical Units".
## Success Criteria
- Accurately converts between units.
- Correctly calculates bin height while accounting for baseplate/bin-bottom thickness.
- Provides clear instructions for "fractional" fits (e.g., if a drawer is 100mm, it fits 2.38 grids).

18
memory-bank/progress.md Normal file
View File

@@ -0,0 +1,18 @@
# Progress Tracking
## Milestones
- [ ] Initial Environment & Docs Setup
- [ ] Core Calculation Engine
- [ ] Web UI & Integration
- [ ] Final Build & Polish
## Todo List
- [x] Create project structure
- [x] Initialize Memory Bank documents
- [ ] Create Blueprint documents (PRD, Architecture)
- [ ] Project setup (Vite, TS, Tailwind, Vitest)
- [ ] Implement `src/calculator.ts`
- [ ] Write tests in `src/calculator.test.ts`
- [ ] Implement `index.html` UI
- [ ] Implement `src/main.ts` glue code
- [ ] Verify build output

View File

@@ -0,0 +1,17 @@
# Project Brief: Gridfinity Calculator
A simple, lightweight web-based calculator for Gridfinity baseplates and bins.
## Core Requirements
- Enter drawer/container dimensions (width, height, depth).
- Support for both inches and millimeters.
- Custom overrides for Gridfinity specifications (default 42x42x7mm).
- Calculate required baseplates (full grids).
- Calculate bin vertical unit height (7mm increments), accounting for base thickness.
- No heavy frameworks (Vanilla TS/Vite).
- Single-page optimized build.
## Goals
- Provide an accurate and easy-to-use tool for Gridfinity makers.
- Allow for non-standard scaling to fit specific drawer sizes perfectly.
- Clean, responsive UI with Tailwind CSS.

View File

@@ -0,0 +1,28 @@
# System Patterns
## Tech Stack
- **Build Tool:** Vite
- **Language:** TypeScript
- **Styling:** Tailwind CSS
- **Testing:** Vitest
## Design Patterns
- **Modular Logic:** Calculation logic is isolated in a pure TypeScript module (`src/calculator.ts`) for easy testing.
- **Reactive UI (Vanilla):** Use DOM event listeners and a central `render()` or `update()` function to sync state to the UI.
- **Unit Normalization:** All internal calculations should be performed in **millimeters**. Inputs in inches are converted immediately upon ingestion.
## Directory Structure
- `src/`: Source code.
- `calculator.ts`: Core business logic.
- `main.ts`: Entry point and DOM manipulation.
- `style.css`: Tailwind entry point.
- `docs/`: Permanent documentation (PRD, Architecture).
- `memory-bank/`: AI operational context.
- `tests/`: Unit tests.
## Calculation Constants (Defaults)
- Grid Width/Depth: 42mm
- Grid Height Unit: 7mm
- Base Thickness: 4.8mm (typical baseplate height above surface)
- Bin Lip/Bottom: 0.7mm (typical clearance/thickness to account for in vertical units)
- *Note: These defaults will be refined during implementation based on official specs.*

2725
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "gridfinity-calc",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest"
},
"devDependencies": {
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vitest": "^0.34.6"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

78
src/calculator.test.ts Normal file
View File

@@ -0,0 +1,78 @@
import { describe, it, expect } from 'vitest';
import { calculateGridfinity, DEFAULT_SPEC } from './calculator';
describe('Gridfinity Calculator', () => {
it('calculates standard 42mm grids correctly in mm', () => {
const results = calculateGridfinity({
width: 84, // 2 grids
length: 126, // 3 grids
height: 50,
unit: 'mm',
spec: DEFAULT_SPEC
});
expect(results.fullGridsX).toBe(2);
expect(results.fullGridsY).toBe(3);
expect(results.widthGrids).toBe(2);
expect(results.lengthGrids).toBe(3);
});
it('calculates fractional grids correctly', () => {
const results = calculateGridfinity({
width: 100, // 100 / 42 = 2.38
length: 42,
height: 50,
unit: 'mm',
spec: DEFAULT_SPEC
});
expect(results.fullGridsX).toBe(2);
expect(results.widthGrids).toBeCloseTo(2.38, 2);
});
it('handles inches correctly', () => {
const results = calculateGridfinity({
width: 1.6536, // Exactly 42.00144mm
length: 3.3071, // 84.00034mm
height: 2,
unit: 'in',
spec: DEFAULT_SPEC
});
expect(results.fullGridsX).toBe(1);
expect(results.fullGridsY).toBe(2);
});
it('calculates max bin units correctly', () => {
// (50mm drawer - 4.8mm base) / 7mm unit = 6.45 -> 6U
const results = calculateGridfinity({
width: 42,
length: 42,
height: 50,
unit: 'mm',
spec: DEFAULT_SPEC
});
expect(results.maxBinUnits).toBe(6);
});
it('respects custom spec overrides', () => {
const customSpec = {
gridSize: 50,
heightUnit: 10,
baseThickness: 5
};
const results = calculateGridfinity({
width: 100,
length: 50,
height: 35,
unit: 'mm',
spec: customSpec
});
expect(results.fullGridsX).toBe(2);
expect(results.fullGridsY).toBe(1);
expect(results.maxBinUnits).toBe(3); // (35 - 5) / 10 = 3
});
});

66
src/calculator.ts Normal file
View File

@@ -0,0 +1,66 @@
export interface GridfinitySpec {
gridSize: number; // e.g., 42mm
heightUnit: number; // e.g., 7mm
baseThickness: number; // Height of baseplate above drawer surface
}
export const DEFAULT_SPEC: GridfinitySpec = {
gridSize: 42,
heightUnit: 7,
baseThickness: 4.8, // Standard baseplate height is 5mm, but usually 4.8mm-5mm
};
export interface CalculatorInputs {
width: number;
length: number;
height: number;
unit: 'mm' | 'in';
spec: GridfinitySpec;
}
export interface CalculatorResults {
widthGrids: number;
lengthGrids: number;
fullGridsX: number;
fullGridsY: number;
maxBinUnits: number;
remainingWidth: number;
remainingLength: number;
remainingHeight: number;
}
const INCH_TO_MM = 25.4;
export function calculateGridfinity(inputs: CalculatorInputs): CalculatorResults {
const { width, length, height, unit, spec } = inputs;
// Convert all to mm for internal calculation
const w = unit === 'in' ? width * INCH_TO_MM : width;
const l = unit === 'in' ? length * INCH_TO_MM : length;
const h = unit === 'in' ? height * INCH_TO_MM : height;
const widthGrids = w / spec.gridSize;
const lengthGrids = l / spec.gridSize;
const fullGridsX = Math.floor(widthGrids);
const fullGridsY = Math.floor(lengthGrids);
// Height calculation: (Available Height - Baseplate Thickness) / 7mm
// We floor this because bins come in whole unit increments
const maxBinUnits = Math.max(0, Math.floor((h - spec.baseThickness) / spec.heightUnit));
const remainingWidth = w % spec.gridSize;
const remainingLength = l % spec.gridSize;
const remainingHeight = (h - spec.baseThickness) % spec.heightUnit;
return {
widthGrids,
lengthGrids,
fullGridsX,
fullGridsY,
maxBinUnits,
remainingWidth,
remainingLength,
remainingHeight
};
}

90
src/main.ts Normal file
View File

@@ -0,0 +1,90 @@
import { calculateGridfinity, GridfinitySpec } from './calculator';
// DOM Elements
const inputWidth = document.getElementById('input-width') as HTMLInputElement;
const inputLength = document.getElementById('input-length') as HTMLInputElement;
const inputHeight = document.getElementById('input-height') as HTMLInputElement;
const unitMmBtn = document.getElementById('unit-mm') as HTMLButtonElement;
const unitInBtn = document.getElementById('unit-in') as HTMLButtonElement;
const specGridSize = document.getElementById('spec-grid-size') as HTMLInputElement;
const specHeightUnit = document.getElementById('spec-height-unit') as HTMLInputElement;
const specBaseThickness = document.getElementById('spec-base-thickness') as HTMLInputElement;
const resultsSection = document.getElementById('results') as HTMLElement;
const resultGridCount = document.getElementById('result-grid-count') as HTMLElement;
const resultGridFractional = document.getElementById('result-grid-fractional') as HTMLElement;
const resultBinHeight = document.getElementById('result-bin-height') as HTMLElement;
const gapWidth = document.getElementById('gap-width') as HTMLElement;
const gapLength = document.getElementById('gap-length') as HTMLElement;
const gapHeight = document.getElementById('gap-height') as HTMLElement;
// State
let currentUnit: 'mm' | 'in' = 'mm';
function updateResults() {
const width = parseFloat(inputWidth.value) || 0;
const length = parseFloat(inputLength.value) || 0;
const height = parseFloat(inputHeight.value) || 0;
const spec: GridfinitySpec = {
gridSize: parseFloat(specGridSize.value) || 42,
heightUnit: parseFloat(specHeightUnit.value) || 7,
baseThickness: parseFloat(specBaseThickness.value) || 4.8,
};
if (width > 0 && length > 0 && height > 0) {
const results = calculateGridfinity({
width,
length,
height,
unit: currentUnit,
spec,
});
resultsSection.classList.remove('hidden');
resultGridCount.textContent = `${results.fullGridsX} x ${results.fullGridsY}`;
resultGridFractional.textContent = `${results.widthGrids.toFixed(2)} x ${results.lengthGrids.toFixed(2)} grids`;
resultBinHeight.textContent = `${results.maxBinUnits}U`;
const unitLabel = currentUnit === 'in' ? 'in' : 'mm';
const displayGapWidth = currentUnit === 'in' ? results.remainingWidth / 25.4 : results.remainingWidth;
const displayGapLength = currentUnit === 'in' ? results.remainingLength / 25.4 : results.remainingLength;
const displayGapHeight = currentUnit === 'in' ? results.remainingHeight / 25.4 : results.remainingHeight;
gapWidth.textContent = `${displayGapWidth.toFixed(1)} ${unitLabel}`;
gapLength.textContent = `${displayGapLength.toFixed(1)} ${unitLabel}`;
gapHeight.textContent = `${displayGapHeight.toFixed(1)} ${unitLabel}`;
} else {
resultsSection.classList.add('hidden');
}
}
// Event Listeners
[inputWidth, inputLength, inputHeight, specGridSize, specHeightUnit, specBaseThickness].forEach(el => {
el.addEventListener('input', updateResults);
});
unitMmBtn.addEventListener('click', () => {
currentUnit = 'mm';
unitMmBtn.classList.add('bg-white', 'shadow-sm');
unitMmBtn.classList.remove('text-slate-600');
unitInBtn.classList.remove('bg-white', 'shadow-sm');
unitInBtn.classList.add('text-slate-600');
updateResults();
});
unitInBtn.addEventListener('click', () => {
currentUnit = 'in';
unitInBtn.classList.add('bg-white', 'shadow-sm');
unitInBtn.classList.remove('text-slate-600');
unitMmBtn.classList.remove('bg-white', 'shadow-sm');
unitMmBtn.classList.add('text-slate-600');
updateResults();
});
// Initial call
updateResults();

3
src/style.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
"include": ["src"]
}