support arbitrary grid sizes; add banana theme

Hunter
Monday, November 17th, 2025 at 11:00 AM

readme.md+8-5
script.js+104-4
style.css+45-29

readme.md
22  ## usage
23  <img src="readme_images/usage.gif">
24  
   -Different Techo types use differently-spaced grid paper. Use the links below to select the appropriate spacing for your Techo.
25 +Different Techo types use differently-sized grid paper. Use the links below to select the appropriate grid size for your Techo.
26  
27  | <b>Techo</b> | <b>Link</b> |
28  |-------------------|-------------|
   -| Planner A6 | [Use 4mm grid ↗](link)<br>(default) |
   -| Original<br>Cousin<br>HON A6<br>HON A5<br>Original Avec<br>Cousin Avec<br>5-Year Techo A6<br>5-Year Techo A5<br>Day-Free A6<br>Day-Free A5 | [Use 3.7mm grid ↗](link) |
   -| Weeks<br>Weeks Mega | [Use 3.55mm grid ↗](link) |
29 +| Planner A6 | [Use 4mm grid ↗](https://hunterirving.github.io/memori?grid-size=4mm)<br>(default) |
30 +| Original<br>Cousin<br>HON A6<br>HON A5<br>Original Avec<br>Cousin Avec<br>5-Year Techo A6<br>5-Year Techo A5<br>Day-Free A6<br>Day-Free A5 | [Use 3.7mm grid ↗](https://hunterirving.github.io/memori?grid-size=3.7mm) |
31 +| Weeks<br>Weeks Mega | [Use 3.55mm grid ↗](https://hunterirving.github.io/memori?grid-size=3.55mm) |
32  
   -After opening memori in your web browser, drag one or more images from your desktop onto the grid.
33  
34 +
35 +### controls
36 +
37 +- After opening memori in your web browser, drag one or more images from your desktop onto the grid.<br>
38  <img src="readme_images/drag_n_drop.gif">
39  
40  - click and drag images to move them<br>
script.js
     -const GRID_COLS = 49;
     -const GRID_ROWS = 66;
     -const CELL_SIZE_MM = 4; // Physical size of each cell when printed
   1 +// Paper dimensions in mm
   2 +const LETTER_WIDTH_MM = 215.9;  // 8.5 inches
   3 +const LETTER_HEIGHT_MM = 279.4; // 11 inches
   4 +const A4_WIDTH_MM = 210;
   5 +const A4_HEIGHT_MM = 297;
   6 +const MARGIN_MM = 6.35; // 0.25 inches
   7 +
   8 +// Calculate maximum grid dimensions that fit both Letter and A4 with margins
   9 +function calculateGridDimensions(cellSize) {
  10 +	// Available printable area (limiting factor is the smaller of Letter/A4 for each dimension)
  11 +	const availableWidth = Math.min(LETTER_WIDTH_MM, A4_WIDTH_MM) - (2 * MARGIN_MM);
  12 +	const availableHeight = Math.min(LETTER_HEIGHT_MM, A4_HEIGHT_MM) - (2 * MARGIN_MM);
  13 +
  14 +	// Calculate how many cells fit
  15 +	const cols = Math.floor(availableWidth / cellSize);
  16 +	const rows = Math.floor(availableHeight / cellSize);
  17 +
  18 +	return { cols, rows };
  19 +}
  20 +
  21 +// Calculate grid dimensions as percentage of Letter paper (used for screen layout)
  22 +function calculateGridPercentages(cellSize, cols, rows) {
  23 +	const gridWidthMM = cols * cellSize;
  24 +	const gridHeightMM = rows * cellSize;
  25 +	return {
  26 +		widthPercent: (gridWidthMM / LETTER_WIDTH_MM) * 100,
  27 +		heightPercent: (gridHeightMM / LETTER_HEIGHT_MM) * 100
  28 +	};
  29 +}
  30 +
  31 +// Cell size bounds (in mm)
  32 +const MIN_CELL_SIZE_MM = 2;
  33 +const MAX_CELL_SIZE_MM = 10;
  34 +
  35 +let GRID_COLS = 49;
  36 +let GRID_ROWS = 66;
  37 +let CELL_SIZE_MM = 4; // Physical size of each cell when printed (can be overridden by URL parameter)
  38  
  39  const grid = document.getElementById('grid');
  40  const page = document.querySelector('.page');

  42  let popupElement = null;
  43  let popupTimeout = null;
  44  
  45 +// Parse URL parameters for custom cell size
  46 +function parseCellSizeFromURL() {
  47 +	const urlParams = new URLSearchParams(window.location.search);
  48 +	const gridSizeParam = urlParams.get('grid-size');
  49 +
  50 +	if (gridSizeParam) {
  51 +		// Remove 'mm' suffix if present
  52 +		const sizeStr = gridSizeParam.toLowerCase().replace('mm', '').trim();
  53 +		const size = parseFloat(sizeStr);
  54 +
  55 +		// Check if size is valid
  56 +		if (isNaN(size)) {
  57 +			showPopup('Invalid grid size. Using default 4mm.', 3000);
  58 +			return 4;
  59 +		}
  60 +
  61 +		// Check if size is within reasonable bounds
  62 +		if (size < MIN_CELL_SIZE_MM || size > MAX_CELL_SIZE_MM) {
  63 +			showPopup(`Grid size must be between ${MIN_CELL_SIZE_MM}mm and ${MAX_CELL_SIZE_MM}mm. Using default 4mm.`, 3500);
  64 +			return 4;
  65 +		}
  66 +
  67 +		// Valid size
  68 +		showPopup(`Grid set to ${size}mm`);
  69 +		return size;
  70 +	}
  71 +
  72 +	return 4; // Default
  73 +}
  74 +
  75 +// Initialize cell size from URL
  76 +CELL_SIZE_MM = parseCellSizeFromURL();
  77 +
  78 +// Calculate grid dimensions based on cell size
  79 +const gridDimensions = calculateGridDimensions(CELL_SIZE_MM);
  80 +GRID_COLS = gridDimensions.cols;
  81 +GRID_ROWS = gridDimensions.rows;
  82 +
  83 +// Calculate grid percentages for screen layout
  84 +const gridPercentages = calculateGridPercentages(CELL_SIZE_MM, GRID_COLS, GRID_ROWS);
  85 +
  86 +// Update CSS variables for both screen and print
  87 +document.documentElement.style.setProperty('--cell-size-mm', `${CELL_SIZE_MM}mm`);
  88 +document.documentElement.style.setProperty('--grid-cols', GRID_COLS);
  89 +document.documentElement.style.setProperty('--grid-rows', GRID_ROWS);
  90 +document.documentElement.style.setProperty('--grid-width-percent', `${gridPercentages.widthPercent}%`);
  91 +document.documentElement.style.setProperty('--grid-height-percent', `${gridPercentages.heightPercent}%`);
  92 +
  93  // Calculate cell size dynamically based on actual grid dimensions
  94  function getCellSize() {
  95  	const gridRect = grid.getBoundingClientRect();

 160  for (let i = 0; i < GRID_COLS * GRID_ROWS; i++) {
 161  	const cell = document.createElement('div');
 162  	cell.className = 'grid-cell';
 163 +
 164 +	// Add right border to rightmost column
 165 +	const col = i % GRID_COLS;
 166 +	if (col === GRID_COLS - 1) {
 167 +		cell.classList.add('right-edge');
 168 +	}
 169 +
 170 +	// Add bottom border to bottom row
 171 +	const row = Math.floor(i / GRID_COLS);
 172 +	if (row === GRID_ROWS - 1) {
 173 +		cell.classList.add('bottom-edge');
 174 +	}
 175 +
 176  	grid.appendChild(cell);
 177  }
 178  
 179 +// Apply solid border style for grids 3.56mm or smaller
 180 +if (CELL_SIZE_MM <= 3.56) {
 181 +	document.documentElement.classList.add('solid-grid');
 182 +}
 183 +
 184  let images = [];
 185  let dragState = null;
 186  let resizeState = null;

1097  // Theme system
1098  let currentThemeIndex = 0;
1099  let isF2Pressed = false;
     -const themes = ['sea-breeze', 'grape-soda', 'grapefruit', 'guac', 'mojito', 'toast'];
1100 +const themes = ['sea-breeze', 'grape-soda', 'grapefruit', 'guac', 'mojito', 'banana'];
1101  
1102  function setTheme(theme) {
1103  	document.documentElement.setAttribute('data-theme', theme);
style.css
 12  	--lime: #a9cf51;
 13  	--avocado: #7a8520;
 14  
    -	/* toast theme colors */
    -	--butter: #ffe598;
    -	--white-wheat: #fffef9;
    -	--crumb: #decca3;
    -	--peanut-butter: #be852a;
 15 +	/* banana theme colors */
 16 +	--peel: #ffcc12;
 17 +	--flesh: #f8f5e3;
 18 +	--unripe: #a5d269;
 19 +	--bruise: #4d4235;
 20  
 21  	/* mojito theme colors */
 22  	--mint: #9febaa;

 56  	--desk: var(--cerulean);
 57  	--page: var(--mist);
 58  	--grid-line: var(--horizon);
 59 +	--grid-line-light: color-mix(in srgb, var(--grid-line) 65%, transparent);
 60  	--accent: var(--lagoon);
 61  
 62  	/* Set text color for all themes */

 74  	--accent: var(--onion);
 75  }
 76  
    -:root[data-theme="toast"] {
    -	--desk: var(--butter);
    -	--page: var(--white-wheat);
    -	--grid-line: var(--crumb);
    -	--accent: var(--peanut-butter);
 77 +:root[data-theme="banana"] {
 78 +	--desk: var(--peel);
 79 +	--page: var(--flesh);
 80 +	--grid-line: var(--unripe);
 81 +	--accent: var(--bruise);
 82  }
 83  
 84  :root[data-theme="mojito"] {

166  	display: block;
167  }
168  
    -/* Add right border to rightmost column (every 49th cell starting from cell 49) */
    -.grid-cell:nth-child(49n) {
169 +/* Add right border to rightmost column */
170 +.grid-cell.right-edge {
171  	border-right: 1px dashed var(--grid-line);
172  	width: calc(100% + 1px);
173  }
174  
    -/* Add bottom border to bottom row (last 49 cells: 3185-3234) */
    -.grid-cell:nth-child(n + 3185) {
175 +/* Add bottom border to bottom row */
176 +.grid-cell.bottom-edge {
177  	border-bottom: 1px dashed var(--grid-line);
178  	height: calc(100% + 1px);
179  }
180  
181 +/* Solid grid lines for cell sizes <= 3.56mm (screen preview only) */
182 +.solid-grid .grid-cell {
183 +	border-top-style: solid;
184 +	border-left-style: solid;
185 +	border-top-color: var(--grid-line-light);
186 +	border-left-color: var(--grid-line-light);
187 +}
188 +
189 +.solid-grid .grid-cell.right-edge {
190 +	border-right-style: solid;
191 +	border-right-color: var(--grid-line-light);
192 +}
193 +
194 +.solid-grid .grid-cell.bottom-edge {
195 +	border-bottom-style: solid;
196 +	border-bottom-color: var(--grid-line-light);
197 +}
198 +
199  @media (max-width: 680px) {
200  	body {
201  		padding: 0;

209  	.grid-cell {
210  		border-top-style: solid;
211  		border-left-style: solid;
    -		border-top-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
    -		border-left-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
212 +		border-top-color: var(--grid-line-light);
213 +		border-left-color: var(--grid-line-light);
214  	}
215  
    -	.grid-cell:nth-child(49n) {
216 +	.grid-cell.right-edge {
217  		border-right-style: solid;
    -		border-right-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
218 +		border-right-color: var(--grid-line-light);
219  		width: calc(100% + 1px);
220  	}
221  
    -	.grid-cell:nth-child(n + 3185) {
222 +	.grid-cell.bottom-edge {
223  		border-bottom-style: solid;
    -		border-bottom-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
224 +		border-bottom-color: var(--grid-line-light);
225  		height: calc(100% + 1px);
226  	}
227  }

502  		padding: 0 !important;
503  	}
504  
    -	.grid-cell:nth-child(49n) {
505 +	.grid-cell.right-edge {
506  		border-right: 1px dashed var(--default-grid) !important;
507  		width: calc(100% + 1px) !important;
508  	}
509  
    -	.grid-cell:nth-child(n + 3185) {
510 +	.grid-cell.bottom-edge {
511  		border-bottom: 1px dashed var(--default-grid) !important;
512  		height: calc(100% + 1px) !important;
513  	}
514  
    -	/* Remove bottom border from cell above bottom-right corner to prevent artifact */
    -	.grid-cell:nth-child(3185) {
    -		border-bottom: none !important;
    -		height: 100% !important;
    -	}
    -
515  	.image-container {
516  		outline: none !important;
517  		position: absolute !important;

556  	.add-images-btn {
557  		display: none !important;
558  	}
559 +
560 +	.popup-notification {
561 +		display: none !important;
562 +	}
563  }
564  
565  @page {

613  	transform: translateX(-50%);
614  	background-color: var(--accent);
615  	color: var(--page);
    -	border: 2px solid var(--page);
616  	padding: 0.7rem 0.9rem;
617  	border-radius: 50rem;
618  	font-size: 1.2rem;