hh-shipments 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "test": "node test/run-tests.js && node test/run-carriers-tests.js"
19
19
  },
20
20
  "type": "commonjs",
21
- "version": "0.3.0",
21
+ "version": "0.3.2",
22
22
  "bin": {
23
23
  "hh-shipments": "server.js"
24
24
  },
package/public/app.js CHANGED
@@ -180,21 +180,65 @@ $('photo-input').addEventListener('change', async (e) => {
180
180
  await handlePhotoFile(f);
181
181
  });
182
182
 
183
- // Paste a screenshot straight from the clipboard (iOS: tap the paste zone, long-press → Paste)
184
- $('tab-photo').addEventListener('paste', async (e) => {
183
+ // Shared: pull image files out of a paste event's clipboard data
184
+ function pastedImageFiles(e) {
185
185
  const dt = e.clipboardData;
186
- if (!dt) return;
187
186
  const files = [];
187
+ if (!dt) return files;
188
188
  if (dt.items) for (const it of dt.items) {
189
189
  if (it.type && it.type.startsWith('image/')) { const f = it.getAsFile(); if (f) files.push(f); }
190
190
  }
191
191
  if (!files.length && dt.files) for (const f of dt.files) {
192
192
  if (f.type && f.type.startsWith('image/')) files.push(f);
193
193
  }
194
- if (!files.length) return; // let text pastes behave normally
194
+ return files;
195
+ }
196
+
197
+ // Paste-text tab: pasting a screenshot runs OCR and extracts tracking numbers;
198
+ // pasting plain text behaves normally
199
+ async function handlePastedImageFile(f) {
200
+ try {
201
+ const text = await ocrFile(f, $('ocr-status-paste'));
202
+ const fd = new FormData();
203
+ fd.append('image', f);
204
+ const up = await fetch('/api/intake/image', { method: 'POST', body: fd });
205
+ if (!up.ok) throw new Error('upload failed');
206
+ await runQuick(text, $('quick-result'));
207
+ } catch (err) { $('ocr-status-paste').textContent = ''; $('quick-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
208
+ }
209
+
210
+ $('tab-paste').addEventListener('paste', async (e) => {
211
+ const files = pastedImageFiles(e);
212
+ if (!files.length) return; // text paste — let it land in the textarea
195
213
  e.preventDefault();
196
- $('paste-zone').innerHTML = '📋 <span class="muted">or tap here, then paste your screenshot</span>';
197
- for (const f of files) await handlePhotoFile(f);
214
+ $('paste-box').innerHTML = '📋 <span class="muted">or tap here, then paste a screenshot</span>';
215
+ for (const f of files) await handlePastedImageFile(f);
216
+ });
217
+
218
+ // Fallback: read the image straight off the clipboard on tap (iOS sometimes
219
+ // won't offer image-paste inside a plain text box)
220
+ $('btn-clipboard').addEventListener('click', async () => {
221
+ if (!navigator.clipboard || !navigator.clipboard.read) {
222
+ $('quick-result').innerHTML = '<p class="error">This browser can\'t read the clipboard — copy the screenshot, tap the dashed box above, long-press → Paste.</p>';
223
+ return;
224
+ }
225
+ try {
226
+ $('ocr-status-paste').textContent = 'Reading clipboard…';
227
+ const items = await navigator.clipboard.read();
228
+ const files = [];
229
+ for (const item of items) for (const type of item.types) {
230
+ if (type.startsWith('image/')) {
231
+ const blob = await item.getType(type);
232
+ files.push(new File([blob], 'clipboard.' + type.split('/')[1], { type }));
233
+ }
234
+ }
235
+ $('ocr-status-paste').textContent = '';
236
+ if (!files.length) { $('quick-result').innerHTML = '<p class="muted">No image on the clipboard — copy a screenshot first, then tap this again.</p>'; return; }
237
+ for (const f of files) await handlePastedImageFile(f);
238
+ } catch (err) {
239
+ $('ocr-status-paste').textContent = '';
240
+ $('quick-result').innerHTML = `<p class="error">Couldn't read the clipboard (${esc(err.message)}). Tap the dashed box above, long-press → Paste instead.</p>`;
241
+ }
198
242
  });
199
243
 
200
244
  async function loadPending() {
@@ -314,7 +358,6 @@ function urlB64(s) {
314
358
  function activateTab(name) {
315
359
  document.querySelectorAll('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === name));
316
360
  ['paste', 'photo', 'manual'].forEach((n) => ($('tab-' + n).hidden = n !== name));
317
- if (name === 'photo') { try { $('paste-zone').focus({ preventScroll: true }); } catch (_) {} }
318
361
  }
319
362
  document.querySelectorAll('.tab').forEach((t) => (t.onclick = () => activateTab(t.dataset.tab)));
320
363
  document.querySelectorAll('#tabbar button').forEach((b) => {
package/public/index.html CHANGED
@@ -62,12 +62,14 @@
62
62
  </div>
63
63
  <div id="tab-paste" class="tabpanel">
64
64
  <textarea id="paste-text" rows="6" placeholder="Paste the seller's message here…"></textarea>
65
+ <div id="paste-box" class="pastebox" contenteditable="true" spellcheck="false">📋 <span class="muted">or tap here, then paste a screenshot</span></div>
66
+ <div class="row" style="margin-top:8px"><button id="btn-clipboard" class="secondary">📋 Grab screenshot from clipboard</button></div>
67
+ <div id="ocr-status-paste" class="muted"></div>
65
68
  <button id="btn-quick" class="primary">Extract &amp; add</button>
66
69
  <div id="quick-result"></div>
67
70
  </div>
68
71
  <div id="tab-photo" class="tabpanel" hidden>
69
72
  <label class="filebtn">📷 Choose screenshot<input id="photo-input" type="file" accept="image/*" hidden></label>
70
- <div id="paste-zone" class="pastezone" contenteditable="true" spellcheck="false" aria-label="Paste a screenshot here">📋 <span class="muted">or tap here, then paste your screenshot</span></div>
71
73
  <div id="ocr-status" class="muted"></div>
72
74
  <div id="photo-result"></div>
73
75
  <h3>Pending screenshots</h3>
package/public/styles.css CHANGED
@@ -42,8 +42,8 @@ h2 { font-size: 17px; margin: 18px 0 8px; }
42
42
  .tab.active { background: var(--blue); color: #fff; }
43
43
  .tabpanel { margin-bottom: 12px; }
44
44
  .filebtn { display: block; text-align: center; background: #e5e5ea; border-radius: 12px; padding: 16px; font-weight: 600; cursor: pointer; }
45
- .pastezone { margin-top: 10px; border: 2px dashed #c7c7cc; border-radius: 12px; padding: 18px; text-align: center; font-size: 15px; min-height: 24px; outline: none; caret-color: transparent; }
46
- .pastezone:focus { border-color: #007aff; background: #f2f8ff; }
45
+ .pastebox { margin-top: 8px; border: 2px dashed #c7c7cc; border-radius: 12px; padding: 14px; text-align: center; outline: none; caret-color: transparent; min-height: 20px; }
46
+ .pastebox:focus { border-color: #007aff; background: #f2f8ff; }
47
47
  .statusline { font-size: 17px; font-weight: 600; margin: 8px 0; }
48
48
  .timeline { border-left: 2px solid #d1d1d6; margin-left: 6px; padding-left: 14px; }
49
49
  .timeline .ev { margin-bottom: 12px; font-size: 14px; }