| Author | djarvis <email> |
|---|---|
| Date | 2026-02-19 12:11:07 GMT-0800 |
| Commit | c49f88404dc44dc3f986812aafd8bb1b67abf097 |
| Parent | 8dec838 |
| -<!DOCTYPE html><!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title></title> | ||
| +<!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title></title> | ||
| </head> | ||
| <body> |
| +<!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title></title> | ||
| +</head> | ||
| +<body> | ||
| +<ul> | ||
| +<li><a href="aero">Aerodynamic Heat Calculator</a></li> | ||
| +<li><a href="payload">Orbital Payload Calculator</a></li> | ||
| +</ol> | ||
| +</body> | ||
| +</html> | ||
| +<?php | ||
| +$filename = isset($_GET['filename']) ? $_GET['filename'] : ''; | ||
| + | ||
| +// Handle annotation POST requests | ||
| +if( $_SERVER['REQUEST_METHOD'] === 'POST' && | ||
| + isset($_POST['annotations']) && | ||
| + isset($_POST['filename']) ) { | ||
| + // Get the directory of the current script | ||
| + $script_dir = dirname(__FILE__); | ||
| + | ||
| + // Ensure target directory exists (relative to this script) | ||
| + $target_dir = $script_dir . "/nn"; | ||
| + | ||
| + if( !is_dir($target_dir) ) { | ||
| + mkdir($target_dir, 0755, true); | ||
| + } | ||
| + | ||
| + // Sanitize the filename to prevent directory traversal | ||
| + $sanitized_filename = basename($_POST['filename']); | ||
| + | ||
| + // Remove .pdf extension if present | ||
| + $sanitized_filename = preg_replace('/\.pdf$/', '', $sanitized_filename); | ||
| + | ||
| + // Full path to the target JSON file | ||
| + $target_file = "$target_dir/{$sanitized_filename}.json"; | ||
| + | ||
| + // Write the annotations data to the file | ||
| + $annotations_data = $_POST['annotations']; | ||
| + file_put_contents($target_file, $annotations_data); | ||
| + | ||
| + // Return a success response | ||
| + header('Content-Type: application/json'); | ||
| + echo json_encode(['success' => true]); | ||
| + exit; | ||
| +} | ||
| + | ||
| +$script_dir = dirname(__FILE__); | ||
| + | ||
| +// Define paths to check | ||
| +$alpha_file = realpath("$script_dir/alpha/$filename"); | ||
| +$beta_file = realpath("$script_dir/beta/$filename"); | ||
| + | ||
| +// Set default path | ||
| +$pdf_path = "/alpha/$filename"; | ||
| + | ||
| +// Check if file exists in alpha directory, otherwise try beta | ||
| +if (!$alpha_file && $beta_file) { | ||
| + $pdf_path = "/beta/$filename"; | ||
| +} | ||
| + | ||
| +// Dynamically generate the base URL | ||
| +$base_url = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') . '/'; | ||
| +?> | ||
| +<!DOCTYPE html> | ||
| +<html dir="ltr" lang="en" mozdisallowselectionprint> | ||
| +<head> | ||
| + <meta charset="utf-8" /> | ||
| + <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| + <base href="<?= $base_url ?>" /> | ||
| + <title>NotaNexus: <?= $filename ?></title> | ||
| + <link rel='stylesheet' type='text/css' href='styles/main.css'> | ||
| +</head> | ||
| +<body tabindex="1"> | ||
| + <div id='pdf' data-filename='<?= $pdf_path ?>'></div> | ||
| + <script type="module" src='js/pdf.min.mjs'></script> | ||
| + <script type='module' src='js/viewer.js'></script> | ||
| +</body> | ||
| +</html> | ||
| + | ||
| +<?xml version="1.0" encoding="utf-8"?> | ||
| +<browserconfig> | ||
| + <msapplication> | ||
| + <tile> | ||
| + <square150x150logo src="/mstile-150x150.png"/> | ||
| + <TileColor>#da532c</TileColor> | ||
| + </tile> | ||
| + </msapplication> | ||
| +</browserconfig> | ||
| +#!/bin/bash | ||
| + | ||
| +# Relative path to location of SVG file to make into ICO file. | ||
| +ICON_PATH=../../images/edible.svg | ||
| + | ||
| +ICON_BASE=$(basename "$ICON_PATH") | ||
| +ICON_DIR=$(dirname "$ICON_PATH") | ||
| +ICON_FILE="${ICON_BASE%*.}" | ||
| +ICON_EXT="${ICON_BASE##*.}" | ||
| + | ||
| +FAVICON_FILE=favicon | ||
| +FAVICON_EXT=.ico | ||
| + | ||
| +# This uses rsvg-convert to create crisp PNG icons. | ||
| +for size in 16 32 64 128 150 192 512; do | ||
| + ICON_OUT=$ICON_FILE-${size}.png | ||
| + DIMENSIONS=${size}x${size} | ||
| + | ||
| + echo "Create ${ICON_OUT}" | ||
| + | ||
| + #rsvg-convert -w $size -p 300 -d 300 $ICON_PATH > $ICON_OUT | ||
| + inkscape \ | ||
| + --batch-process \ | ||
| + -d 300 \ | ||
| + -w $size \ | ||
| + --export-type=png \ | ||
| + -o $ICON_OUT \ | ||
| + $ICON_PATH | ||
| + | ||
| + # Center the image and make it square. | ||
| + convert $ICON_OUT -gravity center -background transparent \ | ||
| + -resize $DIMENSIONS -extent $DIMENSIONS temp-$ICON_OUT | ||
| + | ||
| + # Use 8-bit colour to reduce the file size. | ||
| + pngquant 256 < temp-$ICON_OUT > $FAVICON_FILE-$DIMENSIONS.png | ||
| +done | ||
| + | ||
| +# Create a multi-sized ICO file. | ||
| +convert \ | ||
| + -resize x16 \ | ||
| + -gravity center \ | ||
| + -crop 16x16+0+0 \ | ||
| + -flatten \ | ||
| + -colors 256 \ | ||
| + -background transparent \ | ||
| + -define icon:auto-resize=32,16 \ | ||
| + $FAVICON_FILE-512x512.png \ | ||
| + ../$FAVICON_FILE$FAVICON_EXT | ||
| + | ||
| +# Create Android icons. | ||
| +mv $FAVICON_FILE-192x192.png android-chrome-192x192.png | ||
| +mv $FAVICON_FILE-512x512.png android-chrome-512x512.png | ||
| + | ||
| +# Create MS tile icon. | ||
| +mv $FAVICON_FILE-150x150.png mstile-150x150.png | ||
| + | ||
| +# Clean up the temporary files. | ||
| +rm ${ICON_FILE}*png temp-${ICON_FILE}*png | ||
| + | ||
| - | ||
| +{ | ||
| + "name": "", | ||
| + "icons": [ | ||
| + { | ||
| + "src": "/android-chrome-192x192.png", | ||
| + "sizes": "192x192", | ||
| + "type": "image/png" | ||
| + }, | ||
| + { | ||
| + "src": "/android-chrome-512x512.png", | ||
| + "sizes": "512x512", | ||
| + "type": "image/png" | ||
| + } | ||
| + ], | ||
| + "theme_color": "#ffffff", | ||
| + "background_color": "#ffffff", | ||
| + "display": "standalone" | ||
| +} |
| +<?xml version="1.0" standalone="no"?> | ||
| +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" | ||
| + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> | ||
| +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" | ||
| + width="1056.000000pt" height="1056.000000pt" viewBox="0 0 1056.000000 1056.000000" | ||
| + preserveAspectRatio="xMidYMid meet"> | ||
| +<metadata> | ||
| +Created by potrace 1.11, written by Peter Selinger 2001-2013 | ||
| +</metadata> | ||
| +<g transform="translate(0.000000,1056.000000) scale(0.100000,-0.100000)" | ||
| +fill="#000000" stroke="none"> | ||
| +<path d="M1284 10501 c3 -5 8 -28 11 -52 2 -24 7 -55 9 -69 3 -14 8 -43 11 | ||
| +-65 4 -22 9 -51 11 -65 3 -14 7 -38 9 -55 5 -32 49 -237 64 -295 6 -19 17 -62 | ||
| +26 -95 15 -57 24 -95 29 -122 1 -7 5 -16 9 -19 4 -4 7 -14 7 -23 0 -16 36 | ||
| +-140 50 -176 5 -11 18 -51 31 -90 12 -38 24 -74 28 -80 3 -5 6 -14 7 -20 8 | ||
| +-44 91 -244 180 -433 29 -62 52 -114 51 -115 -1 -1 8 -15 21 -31 12 -16 22 | ||
| +-34 22 -41 0 -7 7 -18 15 -25 8 -7 15 -21 15 -31 0 -11 3 -19 8 -19 4 0 20 | ||
| +-24 36 -53 16 -28 40 -66 53 -83 13 -17 23 -36 23 -42 0 -6 3 -12 8 -14 4 -1 | ||
| +23 -25 42 -53 77 -112 171 -224 280 -334 64 -64 125 -123 137 -133 120 -93 | ||
| +157 -120 211 -157 170 -114 470 -234 642 -257 25 -3 54 -8 65 -11 77 -18 506 | ||
| +-25 625 -11 30 4 73 9 95 12 22 2 63 10 90 16 28 6 59 13 69 16 36 7 121 37 | ||
| +174 60 28 13 55 23 58 23 4 0 10 3 13 8 16 23 115 85 119 74 2 -6 10 -18 18 | ||
| +-25 8 -8 14 -17 14 -20 0 -8 64 -101 73 -104 4 -2 7 -8 7 -14 0 -5 12 -28 28 | ||
| +-51 58 -90 152 -259 152 -276 0 -6 5 -11 12 -11 6 0 9 -3 5 -6 -3 -4 4 -27 17 | ||
| +-53 34 -67 78 -208 80 -256 1 -24 -27 -26 -129 -7 -38 7 -86 15 -105 17 -19 2 | ||
| +-46 6 -60 9 -14 2 -88 14 -165 26 -197 30 -198 30 -215 35 -8 2 -42 7 -75 10 | ||
| +-32 4 -63 9 -67 11 -5 3 -30 7 -56 9 -26 2 -47 4 -47 5 0 2 -11 4 -80 14 -22 | ||
| +3 -51 8 -65 11 -13 2 -42 7 -65 10 -22 3 -67 11 -100 16 -33 6 -78 13 -100 16 | ||
| +-22 2 -58 6 -80 9 -62 8 -262 8 -340 1 -74 -8 -91 -10 -170 -27 -27 -6 -59 | ||
| +-13 -70 -15 -42 -8 -61 -14 -88 -24 -15 -6 -31 -10 -35 -9 -4 1 -16 -2 -27 -6 | ||
| +-11 -5 -63 -24 -115 -44 -52 -20 -97 -37 -100 -37 -15 -3 -166 -81 -168 -87 | ||
| +-2 -5 -9 -8 -16 -8 -10 0 -175 -102 -209 -130 -6 -5 -48 -37 -91 -71 -202 | ||
| +-158 -407 -400 -527 -624 -12 -22 -27 -43 -33 -47 -6 -4 -8 -8 -4 -8 4 0 -7 | ||
| +-30 -26 -67 -50 -102 -119 -283 -132 -348 -2 -11 -13 -54 -24 -95 -18 -65 -36 | ||
| +-160 -54 -280 -21 -134 -27 -434 -12 -575 5 -44 10 -98 12 -120 2 -22 5 -53 8 | ||
| +-70 7 -47 17 -105 22 -128 2 -12 6 -37 9 -57 3 -20 8 -47 11 -60 3 -14 7 -38 | ||
| +9 -55 2 -16 4 -32 5 -35 1 -3 2 -8 1 -12 0 -5 2 -8 6 -8 5 0 9 -10 9 -22 3 | ||
| +-56 81 -386 126 -533 12 -38 21 -74 19 -79 -1 -6 2 -19 8 -30 9 -18 33 -102 | ||
| +46 -161 3 -11 9 -31 14 -45 14 -37 73 -228 73 -237 0 -4 4 -13 8 -18 5 -6 11 | ||
| +-22 14 -35 6 -33 41 -128 49 -137 4 -3 7 -15 7 -25 0 -10 4 -26 10 -36 5 -9 | ||
| +26 -64 46 -122 20 -58 46 -125 57 -150 11 -24 20 -49 22 -55 1 -5 7 -23 13 | ||
| +-38 7 -16 12 -30 12 -33 0 -2 10 -26 21 -52 26 -58 40 -90 73 -169 89 -214 | ||
| +264 -571 364 -745 12 -21 30 -53 41 -70 44 -77 84 -144 121 -200 22 -32 40 | ||
| +-63 40 -68 0 -5 3 -10 8 -12 4 -2 27 -32 50 -67 61 -89 86 -125 93 -131 3 -3 | ||
| +25 -30 48 -60 119 -152 249 -286 382 -395 13 -11 58 -42 99 -70 41 -27 78 -54 | ||
| +81 -58 3 -5 9 -8 13 -7 4 1 31 -10 60 -24 56 -27 112 -47 186 -67 57 -15 228 | ||
| +-20 283 -7 23 5 53 11 67 13 14 2 37 8 53 14 15 6 38 13 50 15 27 6 147 50 | ||
| +173 64 11 6 21 10 24 10 6 1 42 14 115 42 28 10 89 31 137 46 l88 26 37 -19 | ||
| +c175 -90 227 -114 285 -133 37 -12 69 -20 70 -19 2 1 12 -3 24 -9 11 -6 43 | ||
| +-14 70 -18 27 -3 52 -8 56 -10 9 -6 204 -5 238 1 14 2 39 7 55 10 17 4 35 8 | ||
| +40 9 23 5 92 29 122 42 17 8 34 14 37 14 10 0 180 101 187 111 10 17 69 18 | ||
| +124 3 28 -8 57 -16 65 -18 8 -3 16 -5 18 -6 1 -2 5 -3 10 -4 9 -2 65 -23 110 | ||
| +-42 64 -28 103 -44 138 -60 56 -25 201 -73 242 -81 104 -21 304 -2 432 39 36 | ||
| +12 90 33 120 46 30 14 58 26 62 26 4 1 13 7 19 13 7 7 17 13 21 13 17 0 146 | ||
| +86 223 149 84 69 216 199 275 271 19 23 40 48 45 54 6 6 26 31 45 56 19 25 37 | ||
| +47 40 50 3 3 10 13 15 22 6 9 40 62 78 117 37 55 76 115 86 133 10 18 42 72 | ||
| +70 120 57 99 211 410 211 427 0 6 5 11 12 11 6 0 9 3 5 6 -3 3 5 25 18 47 12 | ||
| +23 24 47 25 52 4 15 17 50 40 103 12 26 27 61 35 77 7 17 28 71 47 120 96 248 | ||
| +113 290 114 295 5 28 18 67 24 75 4 6 15 35 24 65 9 30 21 63 27 72 5 10 9 27 | ||
| +9 37 0 11 4 22 9 25 4 3 12 20 16 38 4 18 9 38 11 43 2 6 17 51 34 100 17 50 | ||
| +33 97 35 105 2 8 11 37 20 64 9 26 18 56 21 65 4 19 43 139 51 161 4 8 7 17 8 | ||
| +20 0 3 11 39 25 80 13 41 24 80 26 85 1 6 14 47 29 93 15 46 28 89 29 95 1 7 | ||
| +4 17 8 22 3 6 9 26 13 45 4 19 10 40 13 45 4 6 7 15 8 20 1 6 10 37 19 70 26 | ||
| +88 104 412 110 454 3 20 8 48 11 61 2 14 7 39 9 55 10 70 16 112 20 135 19 | ||
| +120 26 480 12 640 -3 41 -7 75 -7 75 -2 0 -8 36 -25 150 -4 22 -13 61 -20 88 | ||
| +-8 26 -11 47 -7 47 3 0 1 5 -6 12 -7 7 -12 22 -12 34 0 13 -4 26 -10 29 -5 3 | ||
| +-10 16 -10 29 0 12 -3 26 -7 30 -4 3 -8 14 -9 23 -4 24 -68 178 -77 181 -4 2 | ||
| +-7 10 -7 18 0 7 -10 30 -21 51 -12 21 -31 56 -42 78 -22 42 -134 216 -161 250 | ||
| +-9 11 -30 38 -47 60 -17 22 -47 60 -67 85 -42 52 -272 286 -272 277 0 -3 -18 | ||
| +11 -40 31 -53 50 -136 112 -148 112 -6 0 -12 4 -14 9 -3 8 -202 131 -212 131 | ||
| +-2 0 -37 15 -78 34 -40 18 -95 41 -123 50 -27 10 -57 20 -65 23 -8 3 -19 7 | ||
| +-25 8 -5 2 -38 10 -71 19 -34 9 -79 18 -100 21 -22 3 -50 7 -64 10 -52 12 | ||
| +-149 18 -275 19 -147 0 -273 -12 -445 -40 -14 -2 -38 -6 -55 -8 -38 -5 -104 | ||
| +-15 -131 -20 -12 -2 -45 -7 -75 -11 -30 -3 -61 -8 -69 -10 -15 -4 -82 -14 | ||
| +-130 -20 -65 -8 -116 -17 -129 -21 -8 -3 -35 -7 -60 -10 -26 -3 -100 -13 -166 | ||
| +-24 -66 -10 -138 -22 -160 -25 -23 -3 -52 -8 -65 -10 -51 -10 -109 -20 -175 | ||
| +-30 -23 -3 -52 -8 -65 -10 -14 -3 -38 -7 -55 -10 -16 -2 -41 -7 -54 -9 -17 -4 | ||
| +-24 0 -28 14 -3 11 -8 20 -12 20 -4 0 -15 15 -25 33 -51 87 -372 573 -398 601 | ||
| +-10 11 -18 26 -18 33 0 7 -4 13 -10 13 -5 0 -10 4 -10 10 0 5 -6 18 -14 27 -8 | ||
| +10 -49 65 -92 123 -42 58 -80 107 -84 110 -3 3 -17 21 -30 40 -14 19 -34 46 | ||
| +-46 60 -69 80 -114 130 -149 164 -22 22 -31 32 -19 23 11 -10 26 -17 32 -17 7 | ||
| +0 12 -4 12 -10 0 -5 6 -10 14 -10 8 0 41 -17 73 -38 88 -58 91 -58 98 -8 6 36 | ||
| +1 271 -5 271 -2 0 -3 9 -15 75 -17 96 -122 341 -166 389 -11 11 -19 23 -19 27 | ||
| +0 3 -10 19 -22 34 -13 15 -33 42 -46 58 -33 43 -127 138 -192 194 -92 78 -232 | ||
| +178 -251 178 -5 0 -9 3 -9 8 0 4 -10 12 -23 18 -12 6 -74 38 -137 70 -64 33 | ||
| +-131 65 -150 73 -19 8 -53 22 -75 31 -22 10 -56 23 -75 30 -19 7 -44 16 -55 | ||
| +21 -23 9 -115 40 -130 43 -5 1 -32 10 -60 19 -27 9 -61 19 -75 21 -14 3 -47 | ||
| +12 -75 21 -27 9 -66 18 -85 21 -19 3 -42 8 -50 11 -14 5 -68 18 -120 28 -14 3 | ||
| +-41 8 -60 12 -19 4 -42 8 -50 9 -8 1 -26 4 -40 7 -14 3 -70 15 -125 27 -55 11 | ||
| +-111 23 -125 26 -14 3 -29 7 -35 8 -5 1 -37 10 -70 20 -33 10 -64 19 -70 21 | ||
| +-10 2 -21 6 -80 28 -16 6 -34 13 -40 14 -87 19 -432 196 -475 243 -3 3 -21 16 | ||
| +-40 30 -71 50 -179 158 -233 231 -17 24 -34 46 -37 49 -18 18 -90 165 -90 186 | ||
| +0 8 -5 14 -11 14 -5 0 -8 -4 -5 -9z"/> | ||
| +</g> | ||
| +</svg> | ||
| +<head> | ||
| +<link | ||
| + href="//cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" | ||
| + rel="stylesheet" type="text/css" /> | ||
| +<link | ||
| + href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200" | ||
| + rel="stylesheet" /> | ||
| +<link | ||
| + href="//fonts.googleapis.com/css?family=Libre+Baskerville" | ||
| + rel="stylesheet" /> | ||
| +<link | ||
| + href="themes/simple.css" | ||
| + rel="stylesheet" type="text/css" /> | ||
| +</head> | ||
| +<link | ||
| + href="//cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" | ||
| + rel="stylesheet" type="text/css" /> | ||
| +<link | ||
| + href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200" | ||
| + rel="stylesheet" /> | ||
| +<link | ||
| + href="//fonts.googleapis.com/css?family=Libre+Baskerville" | ||
| + rel="stylesheet" /> | ||
| +<link | ||
| + href="themes/simple.css" | ||
| + rel="stylesheet" type="text/css" /> | ||
| + | ||
| +<!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title>autónoma</title><style media="screen" type="text/css"> | ||
| +/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}/*# sourceMappingURL=normalize.min.css.map */ | ||
| + </style><link rel="stylesheet" type="text/css" media="screen" href="themes/simple.css"><link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200|Libre+Baskerville" rel="stylesheet"></head><body><div class="page"><header class="section header"><h1>autónoma</h1></header><nav class="section menu"></nav><main class="section content"><div class="illustration"><img width="408" height="359" src="images/farmer.jpg" alt="Android Farmer"></div><p class="quote"><q>Free the food, free the people.</q> ~ Chloé Angelos, 2058.</p></main><footer class="section footer"></footer></div></body></html> | ||
| +const PAGE_NUMBER_OFFSET = 6; | ||
| + | ||
| +export default class Annotation { | ||
| + constructor(data) { | ||
| + if (typeof data === 'object' && data !== null) { | ||
| + this.id = data.id || this.generateId(); | ||
| + this.text = data.text; | ||
| + this.note = data.note || ''; | ||
| + this.pageNumber = data.pageNumber; | ||
| + this.timestamp = data.timestamp ? new Date(data.timestamp) : new Date(); | ||
| + this.regions = Array.isArray(data.regions) ? [...data.regions] : []; | ||
| + } else { | ||
| + const selectedText = arguments[0]; | ||
| + const pageNumber = arguments[1]; | ||
| + const note = arguments[2] || ''; | ||
| + const existingId = arguments[3] || null; | ||
| + const timestamp = arguments[4] || new Date(); | ||
| + | ||
| + this.id = existingId || this.generateId(); | ||
| + this.text = selectedText; | ||
| + this.note = note; | ||
| + this.pageNumber = pageNumber; | ||
| + this.timestamp = timestamp; | ||
| + this.regions = []; | ||
| + } | ||
| + } | ||
| + | ||
| + generateId() { | ||
| + return Math.floor(1000000 + Math.random() * 10000000); | ||
| + } | ||
| + | ||
| + forEachRegion(callback) { | ||
| + this.regions.forEach((region, index) => { | ||
| + callback(region, index); | ||
| + }); | ||
| + } | ||
| + | ||
| + addPdfRegion(region) { | ||
| + this.regions.push(region); | ||
| + } | ||
| + | ||
| + addRegion(element) { | ||
| + const randomId = this.generateId(); | ||
| + const elementId = element.id || `region-${randomId}`; | ||
| + | ||
| + const existingRegion = this.regions.find(r => r.id === elementId); | ||
| + if (existingRegion) { | ||
| + return; | ||
| + } | ||
| + | ||
| + this.regions.push({ | ||
| + id: elementId, | ||
| + left: parseFloat(element.style.left), | ||
| + top: parseFloat(element.style.top), | ||
| + width: parseFloat(element.style.width), | ||
| + height: parseFloat(element.style.height) | ||
| + }); | ||
| + | ||
| + element.setAttribute('data-annotation-id', this.id); | ||
| + } | ||
| + | ||
| + removeRegion(elementIdOrElement) { | ||
| + const elementId = typeof elementIdOrElement === 'string' ? | ||
| + elementIdOrElement : | ||
| + elementIdOrElement.id; | ||
| + | ||
| + const index = this.regions.findIndex(r => r.id === elementId); | ||
| + if (index !== -1) { | ||
| + this.regions.splice(index, 1); | ||
| + return true; | ||
| + } | ||
| + return false; | ||
| + } | ||
| + | ||
| + hasRegions() { | ||
| + return this.regions.length > 0; | ||
| + } | ||
| + | ||
| + updateNote(newNote) { | ||
| + this.note = newNote; | ||
| + this._log(); | ||
| + } | ||
| + | ||
| + toJson() { | ||
| + return { | ||
| + id: this.id, | ||
| + text: this.text, | ||
| + note: this.note, | ||
| + pageNumber: this.pageNumber, | ||
| + timestamp: this.timestamp, | ||
| + regions: this.regions | ||
| + }; | ||
| + } | ||
| + | ||
| + static fromJson(data) { | ||
| + return new Annotation(data); | ||
| + } | ||
| + | ||
| + save() { | ||
| + this._log(); | ||
| + return this; | ||
| + } | ||
| + | ||
| + toString() { | ||
| + const displayPageNum = parseInt(this.pageNumber) - PAGE_NUMBER_OFFSET; | ||
| + let result = '-'.repeat(5) + `| Page ${displayPageNum} |` + '-'.repeat(30) + '\n'; | ||
| + result += ` > "${this.text}"\n`; | ||
| + result += ` * ${this.note}\n\n`; | ||
| + return result; | ||
| + } | ||
| + | ||
| + _log() { | ||
| + console.log('Annotation:', this.toJson()); | ||
| + } | ||
| +} | ||
| + | ||
| +export default class DialogBox { | ||
| + constructor() { | ||
| + this.dialog = null; | ||
| + this.isDragging = false; | ||
| + this.dragOffset = { x: 0, y: 0 }; | ||
| + | ||
| + this._isEdit = false; | ||
| + this._existingNote = null; | ||
| + this._highlightElement = null; | ||
| + this._onSave = null; | ||
| + this._onCancel = null; | ||
| + this._onDelete = null; | ||
| + this._timestamp = null; | ||
| + } | ||
| + | ||
| + show( | ||
| + selectionRect, | ||
| + isEdit = false, | ||
| + existingNote = null, | ||
| + onSave, | ||
| + onCancel, | ||
| + onDelete, | ||
| + highlightElement = null, | ||
| + timestamp | ||
| + ) { | ||
| + this._removeExistingDialog(); | ||
| + | ||
| + this._isEdit = isEdit; | ||
| + this._existingNote = existingNote; | ||
| + this._highlightElement = highlightElement; | ||
| + this._onSave = onSave; | ||
| + this._onCancel = onCancel; | ||
| + this._onDelete = onDelete; | ||
| + this._timestamp = timestamp; | ||
| + | ||
| + const dialog = document.createElement("div"); | ||
| + dialog.id = "dialog"; | ||
| + dialog.className = "dialog"; | ||
| + | ||
| + this._positionDialog(dialog, selectionRect); | ||
| + | ||
| + const titleBar = this._createTitleBar(); | ||
| + const textarea = this._createTextArea(); | ||
| + | ||
| + dialog.appendChild(titleBar); | ||
| + dialog.appendChild(textarea); | ||
| + | ||
| + document.body.appendChild(dialog); | ||
| + this.dialog = dialog; | ||
| + | ||
| + textarea.focus(); | ||
| + } | ||
| + | ||
| + handleOutsideClick(event) { | ||
| + if (this._isClickOutside(event.target)) { | ||
| + this._processCloseAction(); | ||
| + } | ||
| + } | ||
| + | ||
| + _positionDialog(dialog, selectionRect) { | ||
| + const scrollX = window.scrollX; | ||
| + const scrollY = window.scrollY; | ||
| + dialog.style.left = `${selectionRect.left + scrollX}px`; | ||
| + dialog.style.top = `${selectionRect.bottom + scrollY + 10}px`; | ||
| + } | ||
| + | ||
| + _createTitleBar() { | ||
| + const titleBar = document.createElement("div"); | ||
| + titleBar.className = "dialogTitleBar"; | ||
| + | ||
| + const displayTimestamp = this._formatTimestamp( | ||
| + this._isEdit && this._existingNote | ||
| + ? this._existingNote.timestamp | ||
| + : this._timestamp | ||
| + ); | ||
| + | ||
| + const timestampElement = document.createElement("span"); | ||
| + timestampElement.className = "dialogTimestamp"; | ||
| + timestampElement.textContent = displayTimestamp; | ||
| + | ||
| + const closeButton = document.createElement("span"); | ||
| + closeButton.className = "dialogCloseButton"; | ||
| + closeButton.textContent = "×"; | ||
| + closeButton.addEventListener("click", () => { | ||
| + this._processCloseAction(); | ||
| + }); | ||
| + | ||
| + titleBar.appendChild(timestampElement); | ||
| + titleBar.appendChild(closeButton); | ||
| + this._setupDragHandlers(titleBar); | ||
| + | ||
| + return titleBar; | ||
| + } | ||
| + | ||
| + _createTextArea() { | ||
| + const textarea = document.createElement("textarea"); | ||
| + textarea.placeholder = "Add your note..."; | ||
| + textarea.rows = 3; | ||
| + textarea.className = "dialogTextarea"; | ||
| + | ||
| + if (this._isEdit && this._existingNote) { | ||
| + textarea.value = this._existingNote.note; | ||
| + } | ||
| + | ||
| + textarea.addEventListener("keydown", (event) => { | ||
| + if (event.key === "Escape") { | ||
| + this._processCloseAction(); | ||
| + } | ||
| + }); | ||
| + | ||
| + return textarea; | ||
| + } | ||
| + | ||
| + _processCloseAction() { | ||
| + const noteText = this._getTextContent().trim(); | ||
| + if (noteText === "") { | ||
| + this._onDelete(); | ||
| + } else { | ||
| + this._onSave(noteText); | ||
| + } | ||
| + this._removeExistingDialog(); | ||
| + } | ||
| + | ||
| + _setupDragHandlers(titleBar) { | ||
| + titleBar.style.cursor = "move"; | ||
| + | ||
| + titleBar.addEventListener("mousedown", (e) => { | ||
| + if (e.button === 0 && !e.target.classList.contains("dialogCloseButton")) { | ||
| + this.isDragging = true; | ||
| + const rect = this.dialog.getBoundingClientRect(); | ||
| + this.dragOffset = { | ||
| + x: e.clientX - rect.left, | ||
| + y: e.clientY - rect.top, | ||
| + }; | ||
| + e.preventDefault(); | ||
| + } | ||
| + }); | ||
| + | ||
| + document.addEventListener("mousemove", (e) => { | ||
| + if (this.isDragging) { | ||
| + const x = e.clientX - this.dragOffset.x; | ||
| + const y = e.clientY - this.dragOffset.y; | ||
| + this.dialog.style.left = `${x}px`; | ||
| + this.dialog.style.top = `${y}px`; | ||
| + } | ||
| + }); | ||
| + | ||
| + document.addEventListener("mouseup", () => { | ||
| + this.isDragging = false; | ||
| + }); | ||
| + } | ||
| + | ||
| + _removeExistingDialog() { | ||
| + if (this.dialog) { | ||
| + this.dialog.remove(); | ||
| + this.dialog = null; | ||
| + } | ||
| + } | ||
| + | ||
| + _isClickOutside(target) { | ||
| + return this.dialog && !this.dialog.contains(target); | ||
| + } | ||
| + | ||
| + _getTextContent() { | ||
| + const textarea = this.dialog?.querySelector(".dialogTextarea"); | ||
| + return textarea ? textarea.value : ""; | ||
| + } | ||
| + | ||
| + _formatTimestamp(date) { | ||
| + const formatDate = new Intl.DateTimeFormat("en", { | ||
| + day: "2-digit", | ||
| + month: "short", | ||
| + year: "numeric", | ||
| + hour: "2-digit", | ||
| + minute: "2-digit", | ||
| + second: "2-digit", | ||
| + hour12: false, | ||
| + }); | ||
| + | ||
| + const parts = formatDate.formatToParts(date); | ||
| + return `${parts.find((part) => part.type === "year").value}-${ | ||
| + parts.find((part) => part.type === "month").value | ||
| + }-${parts.find((part) => part.type === "day").value} ${ | ||
| + parts.find((part) => part.type === "hour").value | ||
| + }:${parts.find((part) => part.type === "minute").value}:${ | ||
| + parts.find((part) => part.type === "second").value | ||
| + }`; | ||
| + } | ||
| +} | ||
| + | ||
| +import DialogBox from './DialogBox.js'; | ||
| +import Annotation from './Annotation.js'; | ||
| +import Navigator from './Navigator.js'; | ||
| +import Summary from './Summary.js'; | ||
| + | ||
| +export default class Highlighter { | ||
| + constructor(pdf) { | ||
| + this.pdf = pdf; | ||
| + this.annotations = []; | ||
| + this.currentSelection = { | ||
| + range: null, | ||
| + text: '', | ||
| + pageNumber: null, | ||
| + pageElement: null | ||
| + }; | ||
| + | ||
| + this.dialogBox = new DialogBox(); | ||
| + | ||
| + this.handleTextSelection = this.handleTextSelection.bind(this); | ||
| + this.handleAnnotationClick = this.handleAnnotationClick.bind(this); | ||
| + this.handleDocumentClick = this.handleDocumentClick.bind(this); | ||
| + | ||
| + const path = document.getElementById('pdf').getAttribute('data-filename'); | ||
| + this.filename = path.split('/').pop(); | ||
| + | ||
| + this.loadAnnotationsFromServer(); | ||
| + } | ||
| + | ||
| + setupEventListeners() { | ||
| + document.addEventListener('mouseup', this.handleTextSelection); | ||
| + document.addEventListener('touchend', this.handleTextSelection); | ||
| + document.addEventListener('click', this.handleAnnotationClick); | ||
| + document.addEventListener('mousedown', this.handleDocumentClick); | ||
| + | ||
| + const toolbar = document.createElement('div'); | ||
| + toolbar.className = 'navToolbar'; | ||
| + document.body.appendChild(toolbar); | ||
| + | ||
| + this.navigator = new Navigator(this, toolbar); | ||
| + this.summary = new Summary(this, toolbar); | ||
| + } | ||
| + | ||
| + async loadAnnotationsFromServer() { | ||
| + try { | ||
| + const response = await fetch(`nn/${this.filename.replace('.pdf', '')}.json`, { | ||
| + method: 'GET', | ||
| + headers: { | ||
| + 'Cache-Control': 'no-cache' | ||
| + } | ||
| + }); | ||
| + | ||
| + if (!response.ok) { | ||
| + console.log('No existing annotations found or unable to load annotations'); | ||
| + return; | ||
| + } | ||
| + | ||
| + const annotationsData = await response.json(); | ||
| + | ||
| + for (const item of annotationsData) { | ||
| + this.annotations.push(new Annotation(item)); | ||
| + } | ||
| + | ||
| + this.waitForPdfRenderAndCreateHighlights(annotationsData); | ||
| + | ||
| + console.log(`Loaded ${annotationsData.length} annotations`); | ||
| + } catch (error) { | ||
| + console.error('Error loading annotations:', error); | ||
| + } | ||
| + } | ||
| + | ||
| + waitForPdfRenderAndCreateHighlights(annotationsData) { | ||
| + const checkInterval = 100; | ||
| + const maxAttempts = 50; | ||
| + let attempts = 0; | ||
| + | ||
| + const checkPagesReady = () => { | ||
| + attempts++; | ||
| + const allPages = document.querySelectorAll('.page'); | ||
| + const targetPage = Math.max(...annotationsData.map(a => parseInt(a.pageNumber))); | ||
| + | ||
| + if (allPages.length >= targetPage) { | ||
| + this.createHighlightsFromAnnotations(annotationsData); | ||
| + return; | ||
| + } | ||
| + | ||
| + if (attempts < maxAttempts) { | ||
| + setTimeout(checkPagesReady, checkInterval); | ||
| + } else { | ||
| + console.error('Timed out waiting for PDF pages to render'); | ||
| + } | ||
| + }; | ||
| + | ||
| + setTimeout(checkPagesReady, checkInterval); | ||
| + } | ||
| + | ||
| + createHighlightsFromAnnotations(annotationsData) { | ||
| + for (const annotationData of annotationsData) { | ||
| + const annotation = this.annotations.find(a => a.id === annotationData.id); | ||
| + if (!annotation) continue; | ||
| + | ||
| + const pageNumber = parseInt(annotation.pageNumber); | ||
| + const pageElement = document.querySelector(`.page[data-page-number="${pageNumber}"]`); | ||
| + if (!pageElement) continue; | ||
| + | ||
| + let highlightLayer = pageElement.querySelector('.highlightLayer'); | ||
| + if (!highlightLayer) { | ||
| + highlightLayer = document.createElement('div'); | ||
| + highlightLayer.className = 'highlightLayer'; | ||
| + pageElement.appendChild(highlightLayer); | ||
| + } | ||
| + | ||
| + const page = this.pdf.pages[pageNumber - 1]; | ||
| + if (!page) continue; | ||
| + | ||
| + for (const regionData of annotationData.regions) { | ||
| + const highlight = document.createElement('div'); | ||
| + highlight.className = 'customHighlight'; | ||
| + highlight.id = regionData.id || `highlight-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | ||
| + highlight.setAttribute('data-annotation-id', annotation.id); | ||
| + | ||
| + // Convert PDF coordinates to viewport coordinates | ||
| + const viewport = page.viewport; | ||
| + const rect = viewport.convertToViewportRectangle([ | ||
| + regionData.pdfX, | ||
| + regionData.pdfY, | ||
| + regionData.pdfX + regionData.pdfWidth, | ||
| + regionData.pdfY - regionData.pdfHeight | ||
| + ]); | ||
| + | ||
| + highlight.style.left = Math.min(rect[0], rect[2]) + 'px'; | ||
| + highlight.style.top = Math.min(rect[1], rect[3]) + 'px'; | ||
| + highlight.style.width = Math.abs(rect[2] - rect[0]) + 'px'; | ||
| + highlight.style.height = Math.abs(rect[3] - rect[1]) + 'px'; | ||
| + | ||
| + highlight.addEventListener('mouseover', () => { | ||
| + this.showAnnotationTooltip(annotation, highlight); | ||
| + }); | ||
| + | ||
| + highlight.addEventListener('mouseout', () => { | ||
| + const tooltip = document.getElementById('annotationTooltip'); | ||
| + if (tooltip) { | ||
| + tooltip.remove(); | ||
| + } | ||
| + }); | ||
| + | ||
| + highlightLayer.appendChild(highlight); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + handleDocumentClick(event) { | ||
| + this.dialogBox.handleOutsideClick(event); | ||
| + } | ||
| + | ||
| + handleTextSelection(event) { | ||
| + const selection = window.getSelection(); | ||
| + if (!selection || selection.isCollapsed) return; | ||
| + | ||
| + const selectedText = selection.toString().trim(); | ||
| + if (selectedText.length === 0) return; | ||
| + | ||
| + const textLayers = this.pdf.getTextLayers(); | ||
| + let isWithinTextLayer = false; | ||
| + | ||
| + for (const textLayer of textLayers) { | ||
| + if ( | ||
| + textLayer.contains(selection.anchorNode) || | ||
| + textLayer.contains(selection.focusNode) || | ||
| + this.isTextLayerChild(selection.anchorNode, textLayer) || | ||
| + this.isTextLayerChild(selection.focusNode, textLayer) | ||
| + ) { | ||
| + isWithinTextLayer = true; | ||
| + const pageElement = textLayer.closest('.page'); | ||
| + | ||
| + this.currentSelection = { | ||
| + range: selection.getRangeAt(0).cloneRange(), | ||
| + text: selectedText, | ||
| + pageNumber: pageElement.getAttribute('data-page-number'), | ||
| + pageElement: pageElement | ||
| + }; | ||
| + | ||
| + break; | ||
| + } | ||
| + } | ||
| + | ||
| + if (!isWithinTextLayer) return; | ||
| + | ||
| + const selectionRect = this.currentSelection.range.getBoundingClientRect(); | ||
| + this.showAnnotationPopup(selectionRect); | ||
| + } | ||
| + | ||
| + isTextLayerChild(node, textLayer) { | ||
| + if (!node || !node.parentNode) return false; | ||
| + let parent = node.parentNode; | ||
| + while (parent) { | ||
| + if (parent === textLayer) return true; | ||
| + parent = parent.parentNode; | ||
| + } | ||
| + return false; | ||
| + } | ||
| + | ||
| + handleAnnotationClick(event) { | ||
| + if ( | ||
| + event.target.classList.contains('customHighlight') && | ||
| + event.target.hasAttribute('data-annotation-id') | ||
| + ) { | ||
| + event.stopPropagation(); | ||
| + const annotationId = parseInt( | ||
| + event.target.getAttribute('data-annotation-id') | ||
| + ); | ||
| + const annotation = this.findAnnotationById(annotationId); | ||
| + if (annotation) { | ||
| + const rect = event.target.getBoundingClientRect(); | ||
| + this.showAnnotationPopup(rect, true, annotation, event.target); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + findAnnotationById(id) { | ||
| + return this.annotations.find(a => a.id === id); | ||
| + } | ||
| + | ||
| + showAnnotationPopup( | ||
| + selectionRect, | ||
| + isEdit = false, | ||
| + existingAnnotation = null, | ||
| + highlightElement = null | ||
| + ) { | ||
| + const timestamp = new Date(); | ||
| + | ||
| + const onSave = (annotationText) => { | ||
| + if (isEdit && existingAnnotation) { | ||
| + existingAnnotation.updateNote(annotationText); | ||
| + this.saveAnnotationsToServer('update'); | ||
| + } else { | ||
| + this.createAndSaveAnnotation(annotationText, timestamp); | ||
| + } | ||
| + }; | ||
| + | ||
| + const onDelete = () => { | ||
| + if (isEdit && existingAnnotation) { | ||
| + this.deleteAnnotation(existingAnnotation.id, highlightElement); | ||
| + this.saveAnnotationsToServer('delete'); | ||
| + } else { | ||
| + this.removeHighlights(); | ||
| + } | ||
| + }; | ||
| + | ||
| + const onCancel = () => {}; | ||
| + | ||
| + this.dialogBox.show( | ||
| + selectionRect, | ||
| + isEdit, | ||
| + existingAnnotation, | ||
| + onSave, | ||
| + onCancel, | ||
| + onDelete, | ||
| + highlightElement, | ||
| + timestamp | ||
| + ); | ||
| + | ||
| + if (!isEdit) { | ||
| + this.highlightSelectedText(); | ||
| + } | ||
| + } | ||
| + | ||
| + createAndSaveAnnotation(annotationText, timestamp) { | ||
| + const annotation = new Annotation( | ||
| + this.currentSelection.text, | ||
| + this.currentSelection.pageNumber, | ||
| + annotationText, | ||
| + null, | ||
| + timestamp | ||
| + ); | ||
| + | ||
| + this.annotations.push(annotation); | ||
| + | ||
| + const customHighlights = document.querySelectorAll( | ||
| + '.customHighlight:not([data-annotation-id])' | ||
| + ); | ||
| + | ||
| + const pageNumber = parseInt(this.currentSelection.pageNumber); | ||
| + const page = this.pdf.pages[pageNumber - 1]; | ||
| + const viewport = page.viewport; | ||
| + | ||
| + customHighlights.forEach((highlight) => { | ||
| + // Convert viewport coordinates to PDF coordinates before storing | ||
| + const left = parseFloat(highlight.style.left); | ||
| + const top = parseFloat(highlight.style.top); | ||
| + const width = parseFloat(highlight.style.width); | ||
| + const height = parseFloat(highlight.style.height); | ||
| + | ||
| + // Convert from viewport coordinates to PDF user space coordinates | ||
| + const pdfCoords = viewport.convertToPdfPoint(left, top); | ||
| + const pdfBottomRight = viewport.convertToPdfPoint(left + width, top + height); | ||
| + | ||
| + const pdfRegion = { | ||
| + id: highlight.id, | ||
| + pdfX: pdfCoords[0], | ||
| + pdfY: pdfCoords[1], | ||
| + pdfWidth: pdfBottomRight[0] - pdfCoords[0], | ||
| + pdfHeight: pdfCoords[1] - pdfBottomRight[1], | ||
| + // Keep viewport coordinates for reference | ||
| + left: left, | ||
| + top: top, | ||
| + width: width, | ||
| + height: height | ||
| + }; | ||
| + | ||
| + annotation.addPdfRegion(pdfRegion); | ||
| + | ||
| + highlight.setAttribute('data-annotation-id', annotation.id); | ||
| + | ||
| + highlight.addEventListener('mouseover', () => { | ||
| + this.showAnnotationTooltip(annotation, highlight); | ||
| + }); | ||
| + | ||
| + highlight.addEventListener('mouseout', () => { | ||
| + const tooltip = document.getElementById('annotationTooltip'); | ||
| + if (tooltip) { | ||
| + tooltip.remove(); | ||
| + } | ||
| + }); | ||
| + }); | ||
| + | ||
| + annotation.save(); | ||
| + this.saveAnnotationsToServer('add'); | ||
| + } | ||
| + | ||
| + deleteAnnotation(annotationId, highlightElement) { | ||
| + const annotation = this.findAnnotationById(annotationId); | ||
| + if (!annotation) return; | ||
| + | ||
| + const index = this.annotations.findIndex(a => a.id === annotationId); | ||
| + | ||
| + if (highlightElement) { | ||
| + annotation.removeRegion(highlightElement.id); | ||
| + highlightElement.remove(); | ||
| + } else { | ||
| + const highlights = document.querySelectorAll( | ||
| + `.customHighlight[data-annotation-id="${annotationId}"]` | ||
| + ); | ||
| + highlights.forEach(highlight => { | ||
| + highlight.remove(); | ||
| + }); | ||
| + } | ||
| + | ||
| + if (!annotation.hasRegions()) { | ||
| + this.annotations.splice(index, 1); | ||
| + } | ||
| + } | ||
| + | ||
| + showAnnotationTooltip(annotation, element) { | ||
| + const tooltip = document.createElement('div'); | ||
| + tooltip.id = 'annotationTooltip'; | ||
| + tooltip.className = 'annotationTooltip'; | ||
| + tooltip.textContent = annotation.note; | ||
| + | ||
| + const rect = element.getBoundingClientRect(); | ||
| + const scrollX = window.scrollX; | ||
| + const scrollY = window.scrollY; | ||
| + | ||
| + tooltip.style.left = rect.left + scrollX + 'px'; | ||
| + tooltip.style.top = rect.bottom + scrollY + 5 + 'px'; | ||
| + | ||
| + document.body.appendChild(tooltip); | ||
| + } | ||
| + | ||
| + highlightSelectedText() { | ||
| + if (!this.currentSelection.range) return; | ||
| + | ||
| + const pageNumber = parseInt(this.currentSelection.pageNumber); | ||
| + let highlightLayer = this.currentSelection.pageElement.querySelector('.highlightLayer'); | ||
| + if (!highlightLayer) { | ||
| + highlightLayer = document.createElement('div'); | ||
| + highlightLayer.className = 'highlightLayer'; | ||
| + this.currentSelection.pageElement.appendChild(highlightLayer); | ||
| + } | ||
| + | ||
| + const pageRect = this.currentSelection.pageElement.getBoundingClientRect(); | ||
| + const rects = this.currentSelection.range.getClientRects(); | ||
| + | ||
| + for (let i = 0; i < rects.length; i++) { | ||
| + const rect = rects[i]; | ||
| + | ||
| + const highlight = document.createElement('div'); | ||
| + highlight.className = 'customHighlight'; | ||
| + highlight.id = `highlight-${Date.now()}-${i}`; | ||
| + | ||
| + const relativeLeft = rect.left - pageRect.left; | ||
| + const relativeTop = rect.top - pageRect.top; | ||
| + | ||
| + highlight.style.left = relativeLeft + 'px'; | ||
| + highlight.style.top = relativeTop + 'px'; | ||
| + highlight.style.width = rect.width + 'px'; | ||
| + highlight.style.height = rect.height + 'px'; | ||
| + | ||
| + highlightLayer.appendChild(highlight); | ||
| + } | ||
| + } | ||
| + | ||
| + removeHighlights() { | ||
| + const customHighlights = document.querySelectorAll( | ||
| + '.customHighlight:not([data-annotation-id])' | ||
| + ); | ||
| + customHighlights.forEach((highlight) => { | ||
| + highlight.remove(); | ||
| + }); | ||
| + } | ||
| + | ||
| + forEachAnnotation(callback) { | ||
| + this.annotations.forEach(callback); | ||
| + } | ||
| + | ||
| + _getAnnotations() { | ||
| + return this.annotations.map(annotation => annotation.toJson()); | ||
| + } | ||
| + | ||
| + async saveAnnotationsToServer(action) { | ||
| + try { | ||
| + const annotationsData = this._getAnnotations(); | ||
| + | ||
| + const formData = new FormData(); | ||
| + formData.append('action', action); | ||
| + formData.append('filename', this.filename); | ||
| + formData.append('annotations', JSON.stringify(annotationsData)); | ||
| + | ||
| + const response = await fetch('editor.php', { | ||
| + method: 'POST', | ||
| + body: formData | ||
| + }); | ||
| + | ||
| + if (!response.ok) { | ||
| + throw new Error(`Server responded with status: ${response.status}`); | ||
| + } | ||
| + | ||
| + console.log(`Annotations ${action}d successfully`); | ||
| + } catch (error) { | ||
| + console.error(`Error ${action}ing annotations:`, error); | ||
| + } | ||
| + } | ||
| +} | ||
| +export default class Navigator { | ||
| + constructor(highlighter, toolbar) { | ||
| + this.highlighter = highlighter; | ||
| + this.currentIndex = -1; | ||
| + | ||
| + this._createButtons(toolbar); | ||
| + this._setupEventListeners(); | ||
| + } | ||
| + | ||
| + _createButtons(toolbar) { | ||
| + this.prevButton = document.createElement('button'); | ||
| + this.prevButton.textContent = 'â—€'; | ||
| + this.prevButton.className = 'navButton navPrev'; | ||
| + | ||
| + this.nextButton = document.createElement('button'); | ||
| + this.nextButton.textContent = 'â–¶'; | ||
| + this.nextButton.className = 'navButton navNext'; | ||
| + | ||
| + toolbar.appendChild(this.prevButton); | ||
| + toolbar.appendChild(this.nextButton); | ||
| + } | ||
| + | ||
| + _setupEventListeners() { | ||
| + this.prevButton.addEventListener('click', () => this.backward()); | ||
| + this.nextButton.addEventListener('click', () => this.forward()); | ||
| + } | ||
| + | ||
| + forward() { | ||
| + this._navigate(1); | ||
| + } | ||
| + | ||
| + backward() { | ||
| + this._navigate(-1); | ||
| + } | ||
| + | ||
| + _navigate(direction) { | ||
| + let count = 0; | ||
| + let targetId = null; | ||
| + let firstId = null; | ||
| + let lastId = null; | ||
| + const nextIndex = this.currentIndex + direction; | ||
| + | ||
| + this.highlighter.forEachAnnotation((annotation) => { | ||
| + if (count === 0) firstId = annotation.id; | ||
| + if (count === nextIndex) targetId = annotation.id; | ||
| + lastId = annotation.id; | ||
| + count++; | ||
| + }); | ||
| + | ||
| + if (count === 0) return; | ||
| + | ||
| + if (targetId === null) { | ||
| + targetId = direction > 0 ? firstId : lastId; | ||
| + this.currentIndex = direction > 0 ? 0 : count - 1; | ||
| + } else { | ||
| + this.currentIndex = nextIndex; | ||
| + } | ||
| + | ||
| + this._scrollTo(targetId); | ||
| + } | ||
| + | ||
| + _scrollTo(annotationId) { | ||
| + const highlight = document.querySelector(`[data-annotation-id="${annotationId}"]`); | ||
| + if (highlight) { | ||
| + highlight.scrollIntoView({ block: 'center' }); | ||
| + } | ||
| + } | ||
| +} | ||
| + | ||
| +export default class Page { | ||
| + constructor(viewerElement, pdfPage, pageNum, scale = 1.5) { | ||
| + this.viewerElement = viewerElement; | ||
| + this.pdfPage = pdfPage; | ||
| + this.pageNum = pageNum; | ||
| + this.scale = scale; | ||
| + this.pageElement = null; | ||
| + this.textLayer = null; | ||
| + this.highlightLayer = null; | ||
| + this.viewport = null; | ||
| + } | ||
| + | ||
| + async render() { | ||
| + this.viewport = this.pdfPage.getViewport({ scale: this.scale }); | ||
| + | ||
| + this.pageElement = document.createElement('div'); | ||
| + this.pageElement.className = 'page'; | ||
| + this.pageElement.setAttribute('data-page-number', this.pageNum); | ||
| + this.pageElement.style.width = this.viewport.width + 'px'; | ||
| + this.pageElement.style.height = this.viewport.height + 'px'; | ||
| + | ||
| + this.pageElement.style.setProperty('--scale-factor', this.scale); | ||
| + this.viewerElement.appendChild(this.pageElement); | ||
| + | ||
| + const canvas = document.createElement('canvas'); | ||
| + const context = canvas.getContext('2d'); | ||
| + canvas.width = this.viewport.width; | ||
| + canvas.height = this.viewport.height; | ||
| + this.pageElement.appendChild(canvas); | ||
| + | ||
| + const renderContext = { | ||
| + canvasContext: context, | ||
| + viewport: this.viewport | ||
| + }; | ||
| + | ||
| + await this.pdfPage.render(renderContext).promise; | ||
| + | ||
| + this.textLayer = document.createElement('div'); | ||
| + this.textLayer.className = 'textLayer'; | ||
| + | ||
| + this.textLayer.style.left = '0'; | ||
| + this.textLayer.style.top = '0'; | ||
| + this.textLayer.style.right = '0'; | ||
| + this.textLayer.style.bottom = '0'; | ||
| + this.textLayer.style.position = 'absolute'; | ||
| + this.textLayer.style.transformOrigin = '0% 0%'; | ||
| + | ||
| + this.textLayer.style.setProperty('--scale-factor', this.viewport.scale.toString()); | ||
| + | ||
| + this.pageElement.appendChild(this.textLayer); | ||
| + | ||
| + this.highlightLayer = document.createElement('div'); | ||
| + this.highlightLayer.className = 'highlightLayer'; | ||
| + this.highlightLayer.style.left = '0'; | ||
| + this.highlightLayer.style.top = '0'; | ||
| + this.highlightLayer.style.right = '0'; | ||
| + this.highlightLayer.style.bottom = '0'; | ||
| + this.highlightLayer.style.position = 'absolute'; | ||
| + this.highlightLayer.style.transformOrigin = '0% 0%'; | ||
| + | ||
| + this.pageElement.appendChild(this.highlightLayer); | ||
| + | ||
| + const textLayer = new pdfjsLib.TextLayer({ | ||
| + textContentSource: this.pdfPage.streamTextContent(), | ||
| + container: this.textLayer, | ||
| + viewport: this.viewport | ||
| + }); | ||
| + | ||
| + await textLayer.render(); | ||
| + } | ||
| + | ||
| + getPageElement() { | ||
| + return this.pageElement; | ||
| + } | ||
| + | ||
| + getTextLayer() { | ||
| + return this.textLayer; | ||
| + } | ||
| + | ||
| + getHighlightLayer() { | ||
| + return this.highlightLayer; | ||
| + } | ||
| + | ||
| + getPageNumber() { | ||
| + return this.pageNum; | ||
| + } | ||
| + | ||
| + getViewport() { | ||
| + return this.viewport; | ||
| + } | ||
| +} | ||
| +import Page from './Page.js'; | ||
| +import Highlighter from './Highlighter.js'; | ||
| + | ||
| +const element = ( id ) => document.getElementById( id ); | ||
| + | ||
| +export default class Pdf { | ||
| + constructor( containerId ) { | ||
| + const workerSrc = new URL('./pdf.worker.min.mjs', import.meta.url).href; | ||
| + pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; | ||
| + | ||
| + this.container = element( containerId ); | ||
| + this.url = this.container.getAttribute( 'data-filename' ); | ||
| + this.pages = []; | ||
| + this.scale = this.calculateScale(); | ||
| + | ||
| + window.addEventListener('resize', this.handleResize.bind(this)); | ||
| + | ||
| + this.load(); | ||
| + } | ||
| + | ||
| + calculateScale() { | ||
| + const maxWidth = window.innerWidth - 40; | ||
| + return maxWidth; | ||
| + } | ||
| + | ||
| + handleResize() { | ||
| + const newScale = this.calculateScale(); | ||
| + if (Math.abs(this.scale - newScale) > 20) { | ||
| + this.scale = newScale; | ||
| + this.container.innerHTML = ''; | ||
| + this.pages = []; | ||
| + this.load(); | ||
| + } | ||
| + } | ||
| + | ||
| + async load() { | ||
| + try { | ||
| + const loadingTask = pdfjsLib.getDocument( this.url ); | ||
| + const pdfDoc = await loadingTask.promise; | ||
| + this.pdfDoc = pdfDoc; | ||
| + this.numPages = pdfDoc.numPages; | ||
| + await this.render(); | ||
| + } catch (error) { | ||
| + console.error('Error loading PDF:', error); | ||
| + } | ||
| + } | ||
| + | ||
| + async render() { | ||
| + const firstPage = await this.pdfDoc.getPage(1); | ||
| + const viewport = firstPage.getViewport({ scale: 1.0 }); | ||
| + | ||
| + const scaleFactor = this.scale / viewport.width; | ||
| + | ||
| + for( let pageNum = 1; pageNum <= this.pdfDoc.numPages; pageNum++ ) { | ||
| + try { | ||
| + const pdfPage = await this.pdfDoc.getPage( pageNum ); | ||
| + const page = new Page( this.container, pdfPage, pageNum, scaleFactor ); | ||
| + await page.render(); | ||
| + this.pages.push( page ); | ||
| + } catch (error) { | ||
| + console.error(`Error rendering page ${pageNum}:`, error); | ||
| + } | ||
| + } | ||
| + | ||
| + this.initializeHighlighter(); | ||
| + } | ||
| + | ||
| + initializeHighlighter() { | ||
| + this.highlighter = new Highlighter( this ); | ||
| + this.highlighter.setupEventListeners(); | ||
| + return this.highlighter; | ||
| + } | ||
| + | ||
| + getHighlighter() { | ||
| + return this.highlighter; | ||
| + } | ||
| + | ||
| + getPageCount() { | ||
| + return this.pdfDoc ? this.pdfDoc.numPages : 0; | ||
| + } | ||
| + | ||
| + getPageElements() { | ||
| + return this.pages.map( page => page.getPageElement() ); | ||
| + } | ||
| + | ||
| + getTextLayers() { | ||
| + return this.pages.map( page => page.getTextLayer() ); | ||
| + } | ||
| +} | ||
| + | ||
| +export default class Summary { | ||
| + constructor(highlighter, toolbar) { | ||
| + this.highlighter = highlighter; | ||
| + this.summaryDialog = null; | ||
| + this._createButton(toolbar); | ||
| + } | ||
| + | ||
| + _createButton(toolbar) { | ||
| + this.summaryButton = document.createElement('button'); | ||
| + this.summaryButton.textContent = '📋'; | ||
| + this.summaryButton.className = 'navButton summaryButton'; | ||
| + this.summaryButton.addEventListener('click', () => this.showSummary()); | ||
| + | ||
| + if (toolbar) { | ||
| + toolbar.appendChild(this.summaryButton); | ||
| + } | ||
| + } | ||
| + | ||
| + showSummary() { | ||
| + if (this.summaryDialog) { | ||
| + this.summaryDialog.remove(); | ||
| + } | ||
| + | ||
| + const dialog = document.createElement('div'); | ||
| + dialog.className = 'summaryDialog'; | ||
| + | ||
| + const closeButton = document.createElement('button'); | ||
| + closeButton.textContent = '×'; | ||
| + closeButton.className = 'summaryCloseButton'; | ||
| + closeButton.addEventListener('click', () => this.closeSummary()); | ||
| + | ||
| + const content = document.createElement('textarea'); | ||
| + content.className = 'summaryContent'; | ||
| + content.readOnly = true; | ||
| + content.value = this._generateSummary(); | ||
| + | ||
| + dialog.appendChild(closeButton); | ||
| + dialog.appendChild(content); | ||
| + | ||
| + document.body.appendChild(dialog); | ||
| + this.summaryDialog = dialog; | ||
| + } | ||
| + | ||
| + closeSummary() { | ||
| + if (this.summaryDialog) { | ||
| + this.summaryDialog.remove(); | ||
| + this.summaryDialog = null; | ||
| + } | ||
| + } | ||
| + | ||
| + _generateSummary() { | ||
| + let summary = ""; | ||
| + | ||
| + this.highlighter.forEachAnnotation(annotation => { | ||
| + summary += annotation.toString(); | ||
| + }); | ||
| + | ||
| + return summary.trim(); | ||
| + } | ||
| +} | ||
| + | ||
| +import Pdf from './Pdf.js'; | ||
| + | ||
| +document.addEventListener('DOMContentLoaded', function() { | ||
| + const pdf = new Pdf('pdf'); | ||
| +}); | ||
| + | ||
| +*.json | ||
| + | ||
| +User-agent: googlebot | ||
| +User-agent: google | ||
| +User-agent: bingbot | ||
| +User-agent: bing | ||
| +User-agent: DuckDuckBot/1.0 | ||
| +User-agent: DuckDuckBot/1.1 | ||
| +Disallow: /crawler | ||
| + | ||
| +User-agent: * | ||
| +Disallow: / | ||
| + | ||
| +<?xml version="1.0" encoding="UTF-8"?> | ||
| +<urlset | ||
| + xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" | ||
| + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| + xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 | ||
| + http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> | ||
| +<url> | ||
| + <loc>https://autonoma.ca/</loc> | ||
| + <lastmod>2025-05-21T00:14:53+00:00</lastmod> | ||
| +</url> | ||
| +</urlset> | ||
| +body { | ||
| + margin: 0; | ||
| + padding: 20px; | ||
| + background-color: #1a1a1a; | ||
| +} | ||
| + | ||
| +#pdf { | ||
| + position: relative; | ||
| + margin: 0 auto; | ||
| + background-color: transparent; | ||
| + box-shadow: none; | ||
| +} | ||
| + | ||
| +.page { | ||
| + position: relative; | ||
| + margin-bottom: 20px; | ||
| + overflow: hidden; | ||
| + background-color: #fff; | ||
| + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | ||
| + border-radius: 4px; | ||
| + margin-bottom: 30px; | ||
| +} | ||
| + | ||
| +.textLayer { | ||
| + position: absolute; | ||
| + top: 0; | ||
| + left: 0; | ||
| + right: 0; | ||
| + bottom: 0; | ||
| + overflow: hidden; | ||
| + opacity: 0.2; | ||
| + user-select: text; | ||
| + transform-origin: 0 0; | ||
| + line-height: 1.0; | ||
| +} | ||
| + | ||
| +.textLayer > span, | ||
| +.textLayer > .endOfContent { | ||
| + color: transparent; | ||
| + position: absolute; | ||
| + white-space: pre; | ||
| + cursor: text; | ||
| + transform-origin: 0% 0%; | ||
| +} | ||
| + | ||
| +.textLayer .endOfContent { | ||
| + display: inline-block; | ||
| + pointer-events: none; | ||
| +} | ||
| + | ||
| +.textLayer .highlight { | ||
| + margin: -1px; | ||
| + padding: 1px; | ||
| + background-color: rgba(255, 255, 0, 0.4); | ||
| + border-radius: 4px; | ||
| +} | ||
| + | ||
| +.highlightLayer { | ||
| + position: absolute; | ||
| + top: 0; | ||
| + left: 0; | ||
| + right: 0; | ||
| + bottom: 0; | ||
| + pointer-events: none; | ||
| + transform-origin: 0 0; | ||
| +} | ||
| + | ||
| +.customHighlight { | ||
| + position: absolute; | ||
| + background-color: rgba(255, 255, 0, 0.3); | ||
| + pointer-events: auto; | ||
| + cursor: pointer; | ||
| +} | ||
| + | ||
| +/* Enhanced Dialog Box Styles (without animations) */ | ||
| +.dialog { | ||
| + position: absolute; | ||
| + background-color: white; | ||
| + border-radius: 8px; | ||
| + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); | ||
| + z-index: 1000; | ||
| + overflow: hidden; | ||
| + width: 300px; | ||
| + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||
| +} | ||
| + | ||
| +.dialogTextarea { | ||
| + width: 100%; | ||
| + padding: 12px; | ||
| + resize: vertical; | ||
| + border: 1px solid #eaeaea; | ||
| + border-radius: 4px; | ||
| + box-sizing: border-box; | ||
| + font-family: inherit; | ||
| + font-size: 14px; | ||
| + min-height: 80px; | ||
| + margin: 0 0 10px 0; | ||
| +} | ||
| + | ||
| +.dialogTextarea:focus { | ||
| + outline: none; | ||
| + border-color: #4285f4; | ||
| + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); | ||
| +} | ||
| + | ||
| +.dialogButtonContainer { | ||
| + display: flex; | ||
| + justify-content: flex-end; | ||
| + padding: 0 12px 12px 12px; | ||
| + gap: 8px; | ||
| +} | ||
| + | ||
| +.dialogButton { | ||
| + padding: 8px 16px; | ||
| + border: none; | ||
| + border-radius: 4px; | ||
| + font-size: 14px; | ||
| + font-weight: 500; | ||
| + cursor: pointer; | ||
| +} | ||
| + | ||
| +.saveButton { | ||
| + background-color: #4285f4; | ||
| + color: white; | ||
| +} | ||
| + | ||
| +.saveButton:hover { | ||
| + background-color: #3367d6; | ||
| +} | ||
| + | ||
| +.cancelButton { | ||
| + background-color: #f1f3f4; | ||
| + color: #3c4043; | ||
| +} | ||
| + | ||
| +.cancelButton:hover { | ||
| + background-color: #e8eaed; | ||
| +} | ||
| + | ||
| +.deleteButton { | ||
| + background-color: #ea4335; | ||
| + color: white; | ||
| +} | ||
| + | ||
| +.deleteButton:hover { | ||
| + background-color: #d33426; | ||
| +} | ||
| + | ||
| +.dialogTooltip { | ||
| + position: absolute; | ||
| + background-color: #333; | ||
| + color: white; | ||
| + padding: 8px 12px; | ||
| + border-radius: 4px; | ||
| + max-width: 250px; | ||
| + z-index: 1000; | ||
| + font-size: 14px; | ||
| + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); | ||
| +} | ||
| + | ||
| +.dialogTitleBar { | ||
| + display: flex; | ||
| + justify-content: space-between; | ||
| + align-items: center; | ||
| + padding: 12px; | ||
| + background-color: #f8f9fa; | ||
| + border-bottom: 1px solid #eaeaea; | ||
| + border-radius: 8px 8px 0 0; | ||
| + font-weight: 500; | ||
| + user-select: none; | ||
| +} | ||
| + | ||
| +.dialogCloseButton { | ||
| + cursor: pointer; | ||
| + font-size: 20px; | ||
| + line-height: 1; | ||
| + color: #5f6368; | ||
| + width: 24px; | ||
| + height: 24px; | ||
| + display: flex; | ||
| + align-items: center; | ||
| + justify-content: center; | ||
| + border-radius: 50%; | ||
| +} | ||
| + | ||
| +.dialogCloseButton:hover { | ||
| + background-color: #e8eaed; | ||
| + color: #202124; | ||
| +} | ||
| + | ||
| +.dialogTimestamp { | ||
| + font-size: 13px; | ||
| + color: #5f6368; | ||
| +} | ||
| + | ||
| +.navToolbar { | ||
| + position: fixed; | ||
| + top: 20px; | ||
| + right: 20px; | ||
| + display: flex; | ||
| + gap: 5px; | ||
| + padding: 5px; | ||
| + background: white; | ||
| + border: 1px solid #ccc; | ||
| + border-radius: 4px; | ||
| + z-index: 1000; | ||
| +} | ||
| + | ||
| +.navButton { | ||
| + width: 40px; | ||
| + height: 40px; | ||
| + border: 1px solid #ccc; | ||
| + background: white; | ||
| + font-size: 16px; | ||
| + cursor: pointer; | ||
| +} | ||
| + | ||
| +.summaryDialog { | ||
| + position: fixed; | ||
| + top: 50%; | ||
| + left: 50%; | ||
| + transform: translate(-50%, -50%); | ||
| + width: 600px; | ||
| + max-width: 90vw; | ||
| + height: 500px; | ||
| + max-height: 80vh; | ||
| + background-color: white; | ||
| + border: 1px solid #ccc; | ||
| + padding: 10px; | ||
| + z-index: 2000; | ||
| +} | ||
| + | ||
| +.summaryCloseButton { | ||
| + float: right; | ||
| + border: none; | ||
| + background: none; | ||
| + font-size: 20px; | ||
| + cursor: pointer; | ||
| + padding: 0 5px; | ||
| +} | ||
| + | ||
| +.summaryContent { | ||
| + width: 100%; | ||
| + height: calc(100% - 30px); | ||
| + border: 1px solid #ccc; | ||
| + padding: 10px; | ||
| + font-family: monospace; | ||
| + resize: none; | ||
| +} | ||
| + | ||
| Delta | 1629 lines added, 2 lines removed, 1627-line increase |
|---|