@mblinkov/whats-missing 20.0.27 → 20.0.28

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblinkov/whats-missing",
3
- "version": "20.0.27",
3
+ "version": "20.0.28",
4
4
  "main": "dist/whats-missing.umd.js",
5
5
  "module": "dist/whats-missing.es.js",
6
6
  "types": "dist/index.d.ts",
package/src/Game.tsx CHANGED
@@ -37,7 +37,7 @@ const levenshtein = (a: string, b: string) => {
37
37
  return dp[a.length][b.length];
38
38
  };
39
39
 
40
- // ✅ фикс визуального подзума (например, на 1366×768)
40
+ // ✅ фикс визуального подзума
41
41
  export function useNeutralizeBodyZoom(
42
42
  ref: React.RefObject<HTMLDivElement | null>,
43
43
  opts: { minDesktopWidth?: number; forceResize?: boolean } = { minDesktopWidth: 1200, forceResize: false }
@@ -58,7 +58,6 @@ export function useNeutralizeBodyZoom(
58
58
  if (zoomInline && !isNaN(zoomInline) && zoomInline > 0) return zoomInline;
59
59
  const zoomComputed = parseFloat((cs.zoom as string) || "");
60
60
  if (zoomComputed && !isNaN(zoomComputed) && zoomComputed > 0) return zoomComputed;
61
-
62
61
  const probe = document.createElement("div");
63
62
  probe.style.position = "absolute";
64
63
  probe.style.left = "0";
@@ -66,7 +65,6 @@ export function useNeutralizeBodyZoom(
66
65
  probe.style.width = "100px";
67
66
  probe.style.height = "1px";
68
67
  probe.style.visibility = "hidden";
69
- probe.style.pointerEvents = "none";
70
68
  document.documentElement.appendChild(probe);
71
69
  const rect = probe.getBoundingClientRect();
72
70
  document.documentElement.removeChild(probe);
@@ -270,7 +268,10 @@ export default function Game({
270
268
  setAnswered(true);
271
269
  setResult("wrong");
272
270
  const correct = files[hiddenIndex!].name;
273
- setResultsTable((prev) => [...prev, { round: currentRound, answer: inputValue, correct, result: "wrong" }]);
271
+ setResultsTable((prev) => [
272
+ ...prev,
273
+ { round: currentRound, answer: inputValue, correct, result: "wrong" },
274
+ ]);
274
275
  return;
275
276
  }
276
277
  const t = setTimeout(() => setTimeLeft((s) => s - 1), 1000);
@@ -332,6 +333,17 @@ export default function Game({
332
333
  []
333
334
  );
334
335
 
336
+ // ✅ масштаб интерфейса под реальное разрешение
337
+ const baseWidth = 1440;
338
+ const baseHeight = 900;
339
+ const scale =
340
+ typeof window !== "undefined"
341
+ ? Math.min(
342
+ (screenWidth ?? window.innerWidth) / baseWidth,
343
+ (screenHeight ?? window.innerHeight) / baseHeight
344
+ )
345
+ : 1;
346
+
335
347
  return (
336
348
  <div
337
349
  ref={containerRef}
@@ -342,10 +354,8 @@ export default function Game({
342
354
  justifyContent: "center",
343
355
  alignItems: "center",
344
356
  background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
345
- transition: "background 0.3s ease",
346
357
  overflow: "hidden",
347
358
  zIndex: 1,
348
- pointerEvents: "auto",
349
359
  }}
350
360
  >
351
361
  <div
@@ -357,267 +367,243 @@ export default function Game({
357
367
  height: isMobile
358
368
  ? "100%"
359
369
  : `${Math.min(gameCubeSize ?? 1000, screenWidth ?? 1000, screenHeight ?? 1000)}px`,
360
- maxWidth: "calc(100vw - 24px)",
361
- maxHeight: "calc(100vh - 20px)",
362
- display: "flex",
363
- justifyContent: "center",
364
- alignItems: "center",
365
- overflow: "hidden",
366
370
  borderRadius: isMobile ? 0 : "20px",
367
371
  background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
368
- boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.08)",
369
372
  position: "relative",
373
+ overflow: "hidden",
374
+ boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.08)",
375
+ display: "flex",
376
+ justifyContent: "center",
377
+ alignItems: "center",
370
378
  }}
371
379
  >
372
380
  <div
381
+ id="whats-missing-root"
373
382
  style={{
374
- transform: "none",
375
- transformOrigin: "50% 50%",
376
- width: "100%",
377
- height: "100%",
383
+ transform: `scale(${scale})`,
384
+ transformOrigin: "center center",
385
+ width: `${100 / scale}%`,
386
+ height: `${100 / scale}%`,
378
387
  display: "flex",
388
+ flexDirection: "column",
379
389
  justifyContent: "center",
380
390
  alignItems: "center",
381
391
  }}
382
392
  >
383
- <div id="whats-missing-root">
384
- {!isMobile && MemoizedLogo}
385
- {/* ====== ЛОББИ ====== */}
386
- {!theme && !started && (
387
- <div style={styles.gmCenterScreen}>
388
- <h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
389
- <p style={styles.gmBodyM}>Select a theme:</p>
390
- <div style={{ display: "flex", gap: 16 }}>
391
- {["animals", "food", "toys"].map((t) => (
393
+ {!isMobile && MemoizedLogo}
394
+
395
+ {/* ====== ЛОББИ ====== */}
396
+ {!theme && !started && (
397
+ <div style={styles.gmCenterScreen}>
398
+ <h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
399
+ <p style={styles.gmBodyM}>Select a theme:</p>
400
+ <div style={{ display: "flex", gap: 16 }}>
401
+ {["animals", "food", "toys"].map((t) => (
402
+ <button key={t} style={styles.gmButton} onClick={() => setTheme(t as Theme)}>
403
+ {t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
404
+ </button>
405
+ ))}
406
+ </div>
407
+ <div style={{ marginTop: 24 }}>
408
+ <p style={styles.gmBodyS}>Choose number of rounds:</p>
409
+ <div style={{ display: "flex", gap: 12, marginTop: 8 }}>
410
+ {[3, 4, 5].map((n) => (
392
411
  <button
393
- key={t}
394
- style={styles.gmButton}
395
- onClick={() => setTheme(t as Theme)}
412
+ key={n}
413
+ style={{
414
+ ...styles.gmButton,
415
+ ...(rounds === n ? styles.gmButtonActive : {}),
416
+ }}
417
+ onClick={() => setRounds(n)}
396
418
  >
397
- {t === "animals"
398
- ? "🐶 Animals"
399
- : t === "food"
400
- ? "🍎 Food"
401
- : "🧸 Toys"}
419
+ {n}
402
420
  </button>
403
421
  ))}
404
422
  </div>
405
- <div style={{ marginTop: 24 }}>
406
- <p style={styles.gmBodyS}>Choose number of rounds:</p>
407
- <div style={{ display: "flex", gap: 12, marginTop: 8 }}>
408
- {[3, 4, 5].map((n) => (
409
- <button
410
- key={n}
411
- style={{
412
- ...styles.gmButton,
413
- ...(rounds === n ? styles.gmButtonActive : {}),
414
- }}
415
- onClick={() => setRounds(n)}
416
- >
417
- {n}
418
- </button>
419
- ))}
420
- </div>
421
- </div>
422
423
  </div>
423
- )}
424
- {/* ====== ПОДТВЕРЖДЕНИЕ ====== */}
425
- {theme && !started && (
426
- <div style={styles.gmCenterScreen}>
427
- <h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
428
- <p style={styles.gmBodyM}>Rounds: {rounds}</p>
424
+ </div>
425
+ )}
426
+
427
+ {/* ====== ПОДТВЕРЖДЕНИЕ ====== */}
428
+ {theme && !started && (
429
+ <div style={styles.gmCenterScreen}>
430
+ <h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
431
+ <p style={styles.gmBodyM}>Rounds: {rounds}</p>
432
+ <button style={styles.gmButton} onClick={startGame}>
433
+ ▶ Start game
434
+ </button>
435
+ </div>
436
+ )}
437
+
438
+ {/* ====== РЕЗУЛЬТАТЫ ====== */}
439
+ {finished && (
440
+ <div style={styles.gmCenterScreen}>
441
+ <h1 style={styles.gmHeadline1}>Results</h1>
442
+ <h2 style={styles.gmHeadline3}>
443
+ Your score: {score} / {rounds}
444
+ </h2>
445
+ <p style={{ ...styles.gmBodyM, color: "#10b981", marginTop: 12 }}>
446
+ Yahoo! You did it! 🍬✨
447
+ </p>
448
+ <table style={styles.gmTable}>
449
+ <thead>
450
+ <tr>
451
+ <th>Round</th>
452
+ <th>Your Answer</th>
453
+ <th>Correct</th>
454
+ <th>Result</th>
455
+ </tr>
456
+ </thead>
457
+ <tbody>
458
+ {resultsTable.map((r, i) => (
459
+ <tr key={i}>
460
+ <td style={styles.gmTableCell}>{r.round}</td>
461
+ <td style={styles.gmTableCell}>{r.answer || "—"}</td>
462
+ <td style={styles.gmTableCell}>{r.correct}</td>
463
+ <td style={styles.gmTableCell}>
464
+ {r.result === "correct"
465
+ ? "✔ Correct"
466
+ : r.result === "almost"
467
+ ? "◐ Almost (0.5)"
468
+ : "✘ Wrong"}
469
+ </td>
470
+ </tr>
471
+ ))}
472
+ </tbody>
473
+ </table>
474
+ <div style={{ display: "flex", gap: 12, marginTop: 24 }}>
429
475
  <button style={styles.gmButton} onClick={startGame}>
430
- Start game
476
+ 🔁 Play again
477
+ </button>
478
+ <button style={styles.gmButton} onClick={exitGame}>
479
+ ⬅️ Choose theme
431
480
  </button>
432
481
  </div>
433
- )}
434
- {/* ====== РЕЗУЛЬТАТЫ ====== */}
435
- {finished && (
436
- <div style={styles.gmCenterScreen}>
437
- <h1 style={styles.gmHeadline1}>Results</h1>
438
- <h2 style={styles.gmHeadline3}>
439
- Your score: {score} / {rounds}
440
- </h2>
441
- <p style={{ ...styles.gmBodyM, color: "#10b981", marginTop: 12 }}>
442
- Yahoo! You did it! 🍬✨
443
- </p>
444
- <table style={styles.gmTable}>
445
- <thead>
446
- <tr>
447
- <th>Round</th>
448
- <th>Your Answer</th>
449
- <th>Correct</th>
450
- <th>Result</th>
451
- </tr>
452
- </thead>
453
- <tbody>
454
- {resultsTable.map((r, i) => (
455
- <tr key={i}>
456
- <td style={styles.gmTableCell}>{r.round}</td>
457
- <td style={styles.gmTableCell}>
458
- {r.answer || "—"}
459
- </td>
460
- <td style={styles.gmTableCell}>{r.correct}</td>
461
- <td style={styles.gmTableCell}>
462
- {r.result === "correct"
463
- ? "✔ Correct"
464
- : r.result === "almost"
465
- ? "◐ Almost (0.5)"
466
- : "✘ Wrong"}
467
- </td>
468
- </tr>
469
- ))}
470
- </tbody>
471
- </table>
472
- <div style={{ display: "flex", gap: 12, marginTop: 24 }}>
473
- <button style={styles.gmButton} onClick={startGame}>
474
- 🔁 Play again
475
- </button>
476
- <button style={styles.gmButton} onClick={exitGame}>
477
- ⬅️ Choose theme
478
- </button>
479
- </div>
482
+ </div>
483
+ )}
484
+
485
+ {/* ====== ИГРОВОЙ ЭКРАН ====== */}
486
+ {started && !finished && (
487
+ <div style={styles.gmGameLayout}>
488
+ <div
489
+ style={{
490
+ minHeight: 160,
491
+ display: "flex",
492
+ flexDirection: "column",
493
+ justifyContent: "center",
494
+ alignItems: "center",
495
+ }}
496
+ >
497
+ {phase === "ready" && (
498
+ <>
499
+ <h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
500
+ <div style={styles.gmHourglass}>⏳</div>
501
+ </>
502
+ )}
503
+ {phase === "memorize" && (
504
+ <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
505
+ MEMORIZE ({memorizeTime})
506
+ </p>
507
+ )}
508
+ {phase === "guess" && !answered && (
509
+ <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
510
+ )}
480
511
  </div>
481
- )}
482
- {/* ====== ИГРОВОЙ ЭКРАН ====== */}
483
- {started && !finished && (
484
- <div style={styles.gmGameLayout}>
485
- <div
486
- style={{
487
- minHeight: 160,
488
- display: "flex",
489
- flexDirection: "column",
490
- justifyContent: "center",
491
- alignItems: "center",
492
- }}
493
- >
494
- {phase === "ready" && (
495
- <>
496
- <h1
497
- style={{ ...styles.gmHeadline1, color: "#ec4c44" }}
498
- >
499
- GET READY
500
- </h1>
501
- <div style={styles.gmHourglass}>⏳</div>
502
- </>
503
- )}
504
- {phase === "memorize" && (
505
- <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
506
- MEMORIZE ({memorizeTime})
507
- </p>
508
- )}
509
- {phase === "guess" && !answered && (
510
- <p style={styles.gmBodyM}>
511
- Time left: {timeLeft}s
512
- </p>
513
- )}
514
- </div>
515
- {/* Сетка карточек */}
516
- {phase !== "ready" && (() => {
517
- const gap = isMobile ? 12 : 20;
518
- const innerGutters = 32;
519
- const effective = Math.min(
520
- gameCubeSize ?? 1000,
521
- squareSize ?? 1000,
522
- screenHeight ?? 1000,
523
- screenWidth ?? 1000
524
- );
525
- const columns = isMobile ? 2 : 3;
526
- const cardSize = Math.max(
527
- 90,
528
- Math.floor(
529
- (effective - innerGutters - gap * (columns - 1)) /
530
- columns
531
- )
532
- );
533
- return (
534
- <div
535
- style={{
536
- ...styles.gmGrid,
537
- gridTemplateColumns: `repeat(${columns}, ${cardSize}px)`,
538
- gridAutoRows: `${cardSize}px`,
539
- gap: `${gap}px`,
540
- justifyContent: "center",
541
- justifyItems: "center",
542
- padding: 8,
543
- boxSizing: "border-box",
544
- }}
545
- >
546
- {files.map((file, i) => {
547
- const isHidden =
548
- hiddenIndex === i &&
549
- phase === "guess" &&
550
- !answered;
551
- return (
552
- <div
553
- key={i}
554
- style={{
555
- ...styles.gmCard,
556
- width: `${cardSize}px`,
557
- height: `${cardSize}px`,
558
- padding: isMobile ? 8 : 10,
559
- boxSizing: "border-box",
560
- ...(result === "correct" &&
561
- hiddenIndex === i
562
- ? styles.gmCorrect
563
- : {}),
564
- ...(result === "wrong" &&
565
- hiddenIndex === i
566
- ? styles.gmWrong
567
- : {}),
568
- }}
569
- >
570
- {!isHidden && (
571
- <img
572
- src={file.src}
573
- alt={file.name}
574
- style={{
575
- width: "100%",
576
- height: "100%",
577
- objectFit: "cover",
578
- display: "block",
579
- }}
580
- />
581
- )}
582
- </div>
583
- );
584
- })}
585
- </div>
586
- );
587
- })()}
588
- {/* Инпут и кнопки */}
589
- <div style={{ marginTop: 16, height: 80 }}>
590
- {phase === "guess" && !answered && (
591
- <div>
592
- <input
593
- type="text"
594
- placeholder="Type the missing word"
595
- value={inputValue}
596
- onChange={(e) => setInputValue(e.target.value)}
597
- style={styles.gmInput}
598
- />
599
- <button
600
- style={{ ...styles.gmButton, marginLeft: 8 }}
601
- onClick={checkAnswer}
602
- disabled={animating}
603
- >
604
- {animating ? "..." : "Check"}
605
- </button>
606
- </div>
607
- )}
608
- {answered && (
512
+
513
+ {/* Сетка карточек */}
514
+ {phase !== "ready" && (() => {
515
+ const gap = isMobile ? 12 : 20;
516
+ const innerGutters = 32;
517
+ const effective = Math.min(
518
+ squareRef.current?.clientWidth ?? gameCubeSize ?? 1000,
519
+ squareRef.current?.clientHeight ?? screenHeight ?? 1000,
520
+ screenWidth ?? 1000
521
+ );
522
+ const columns = isMobile ? 2 : 3;
523
+ const cardSize = Math.max(
524
+ 90,
525
+ Math.floor((effective - innerGutters - gap * (columns - 1)) / columns)
526
+ );
527
+
528
+ return (
529
+ <div
530
+ style={{
531
+ ...styles.gmGrid,
532
+ gridTemplateColumns: `repeat(${columns}, ${cardSize}px)`,
533
+ gridAutoRows: `${cardSize}px`,
534
+ gap: `${gap}px`,
535
+ justifyContent: "center",
536
+ justifyItems: "center",
537
+ padding: 8,
538
+ boxSizing: "border-box",
539
+ }}
540
+ >
541
+ {files.map((file, i) => {
542
+ const isHidden = hiddenIndex === i && phase === "guess" && !answered;
543
+ return (
544
+ <div
545
+ key={i}
546
+ style={{
547
+ ...styles.gmCard,
548
+ width: `${cardSize}px`,
549
+ height: `${cardSize}px`,
550
+ padding: isMobile ? 8 : 10,
551
+ boxSizing: "border-box",
552
+ ...(result === "correct" && hiddenIndex === i
553
+ ? styles.gmCorrect
554
+ : {}),
555
+ ...(result === "wrong" && hiddenIndex === i
556
+ ? styles.gmWrong
557
+ : {}),
558
+ }}
559
+ >
560
+ {!isHidden && (
561
+ <img
562
+ src={file.src}
563
+ alt={file.name}
564
+ style={{
565
+ width: "100%",
566
+ height: "100%",
567
+ objectFit: "cover",
568
+ display: "block",
569
+ }}
570
+ />
571
+ )}
572
+ </div>
573
+ );
574
+ })}
575
+ </div>
576
+ );
577
+ })()}
578
+
579
+ {/* Инпут и кнопки */}
580
+ <div style={{ marginTop: 16, height: 80 }}>
581
+ {phase === "guess" && !answered && (
582
+ <div>
583
+ <input
584
+ type="text"
585
+ placeholder="Type the missing word"
586
+ value={inputValue}
587
+ onChange={(e) => setInputValue(e.target.value)}
588
+ style={styles.gmInput}
589
+ />
609
590
  <button
610
- type="button"
611
- style={styles.gmButton}
612
- onClick={nextRound}
591
+ style={{ ...styles.gmButton, marginLeft: 8 }}
592
+ onClick={checkAnswer}
593
+ disabled={animating}
613
594
  >
614
- Next round
595
+ {animating ? "..." : "Check"}
615
596
  </button>
616
- )}
617
- </div>
597
+ </div>
598
+ )}
599
+ {answered && (
600
+ <button type="button" style={styles.gmButton} onClick={nextRound}>
601
+ Next round
602
+ </button>
603
+ )}
618
604
  </div>
619
- )}
620
- </div>
605
+ </div>
606
+ )}
621
607
  </div>
622
608
  </div>
623
609
  </div>