@mblinkov/whats-missing 20.0.26 → 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.26",
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);
@@ -303,7 +304,8 @@ export default function Game({
303
304
  }, 600);
304
305
  };
305
306
 
306
- const nextRound = () => (currentRound < rounds ? (setCurrentRound((r) => r + 1), startRound()) : setFinished(true));
307
+ const nextRound = () =>
308
+ currentRound < rounds ? (setCurrentRound((r) => r + 1), startRound()) : setFinished(true);
307
309
 
308
310
  const exitGame = () => {
309
311
  setStarted(false);
@@ -315,14 +317,33 @@ export default function Game({
315
317
  () => (
316
318
  <div style={{ ...styles.gmLogoFixed, position: "absolute", top: 16, left: 16, zIndex: 30 }}>
317
319
  <picture>
318
- <source srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"} type="image/svg+xml" />
319
- <img src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"} alt="SPEAKID Logo" style={styles.gmLogoImg} loading="lazy" />
320
+ <source
321
+ srcSet={window.origin + "/cloud/speakid/games/whatsmissing/logo.svg"}
322
+ type="image/svg+xml"
323
+ />
324
+ <img
325
+ src={window.origin + "/cloud/speakid/games/whatsmissing/logo.png"}
326
+ alt="SPEAKID Logo"
327
+ style={styles.gmLogoImg}
328
+ loading="lazy"
329
+ />
320
330
  </picture>
321
331
  </div>
322
332
  ),
323
333
  []
324
334
  );
325
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
+
326
347
  return (
327
348
  <div
328
349
  ref={containerRef}
@@ -333,241 +354,256 @@ export default function Game({
333
354
  justifyContent: "center",
334
355
  alignItems: "center",
335
356
  background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
336
- transition: "background 0.3s ease",
337
357
  overflow: "hidden",
338
358
  zIndex: 1,
339
- pointerEvents: "auto",
340
359
  }}
341
360
  >
342
361
  <div
343
362
  ref={squareRef}
344
363
  style={{
345
- width: gameCubeSize ? `${gameCubeSize}px` : isMobile ? "100%" : "1000px",
346
- height: gameCubeSize ? `${gameCubeSize}px` : isMobile ? "100%" : "1000px",
347
- maxWidth: "calc(100vw - 24px)",
348
- maxHeight: "calc(100vh - 20px)",
349
- display: "flex",
350
- justifyContent: "center",
351
- alignItems: "center",
352
- overflow: "hidden",
364
+ width: isMobile
365
+ ? "100%"
366
+ : `${Math.min(gameCubeSize ?? 1000, screenWidth ?? 1000, screenHeight ?? 1000)}px`,
367
+ height: isMobile
368
+ ? "100%"
369
+ : `${Math.min(gameCubeSize ?? 1000, screenWidth ?? 1000, screenHeight ?? 1000)}px`,
353
370
  borderRadius: isMobile ? 0 : "20px",
354
371
  background: "linear-gradient(to bottom, #fff8f8 0%, #f9fafb 100%)",
355
- boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.08)",
356
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",
357
378
  }}
358
379
  >
359
380
  <div
381
+ id="whats-missing-root"
360
382
  style={{
361
- transform: "none",
362
- transformOrigin: "50% 50%",
363
- width: "100%",
364
- height: "100%",
383
+ transform: `scale(${scale})`,
384
+ transformOrigin: "center center",
385
+ width: `${100 / scale}%`,
386
+ height: `${100 / scale}%`,
365
387
  display: "flex",
388
+ flexDirection: "column",
366
389
  justifyContent: "center",
367
390
  alignItems: "center",
368
391
  }}
369
392
  >
370
- <div id="whats-missing-root">
371
- {!isMobile && MemoizedLogo}
372
-
373
- {/* ====== ЛОББИ ====== */}
374
- {!theme && !started && (
375
- <div style={styles.gmCenterScreen}>
376
- <h1 style={styles.gmHeadline1}>WHAT'S MISSING?</h1>
377
- <p style={styles.gmBodyM}>Select a theme:</p>
378
- <div style={{ display: "flex", gap: 16 }}>
379
- {["animals", "food", "toys"].map((t) => (
380
- <button key={t} style={styles.gmButton} onClick={() => setTheme(t as Theme)}>
381
- {t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
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) => (
411
+ <button
412
+ key={n}
413
+ style={{
414
+ ...styles.gmButton,
415
+ ...(rounds === n ? styles.gmButtonActive : {}),
416
+ }}
417
+ onClick={() => setRounds(n)}
418
+ >
419
+ {n}
382
420
  </button>
383
421
  ))}
384
422
  </div>
385
- <div style={{ marginTop: 24 }}>
386
- <p style={styles.gmBodyS}>Choose number of rounds:</p>
387
- <div style={{ display: "flex", gap: 12, marginTop: 8 }}>
388
- {[3, 4, 5].map((n) => (
389
- <button
390
- key={n}
391
- style={{
392
- ...styles.gmButton,
393
- ...(rounds === n ? styles.gmButtonActive : {}),
394
- }}
395
- onClick={() => setRounds(n)}
396
- >
397
- {n}
398
- </button>
399
- ))}
400
- </div>
401
- </div>
402
423
  </div>
403
- )}
404
-
405
- {/* ====== ПОДТВЕРЖДЕНИЕ ====== */}
406
- {theme && !started && (
407
- <div style={styles.gmCenterScreen}>
408
- <h1 style={styles.gmHeadline1}>Theme selected: {theme}</h1>
409
- <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 }}>
410
475
  <button style={styles.gmButton} onClick={startGame}>
411
- Start game
476
+ 🔁 Play again
477
+ </button>
478
+ <button style={styles.gmButton} onClick={exitGame}>
479
+ ⬅️ Choose theme
412
480
  </button>
413
481
  </div>
414
- )}
415
-
416
- {/* ====== РЕЗУЛЬТАТЫ ====== */}
417
- {finished && (
418
- <div style={styles.gmCenterScreen}>
419
- <h1 style={styles.gmHeadline1}>Results</h1>
420
- <h2 style={styles.gmHeadline3}>
421
- Your score: {score} / {rounds}
422
- </h2>
423
- <p style={{ ...styles.gmBodyM, color: "#10b981", marginTop: 12 }}>Yahoo! You did it! 🍬✨</p>
424
- <table style={styles.gmTable}>
425
- <thead>
426
- <tr>
427
- <th>Round</th>
428
- <th>Your Answer</th>
429
- <th>Correct</th>
430
- <th>Result</th>
431
- </tr>
432
- </thead>
433
- <tbody>
434
- {resultsTable.map((r, i) => (
435
- <tr key={i}>
436
- <td style={styles.gmTableCell}>{r.round}</td>
437
- <td style={styles.gmTableCell}>{r.answer || "—"}</td>
438
- <td style={styles.gmTableCell}>{r.correct}</td>
439
- <td style={styles.gmTableCell}>
440
- {r.result === "correct"
441
- ? "✔ Correct"
442
- : r.result === "almost"
443
- ? "◐ Almost (0.5)"
444
- : "✘ Wrong"}
445
- </td>
446
- </tr>
447
- ))}
448
- </tbody>
449
- </table>
450
- <div style={{ display: "flex", gap: 12, marginTop: 24 }}>
451
- <button style={styles.gmButton} onClick={startGame}>
452
- 🔁 Play again
453
- </button>
454
- <button style={styles.gmButton} onClick={exitGame}>
455
- ⬅️ Choose theme
456
- </button>
457
- </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
+ )}
458
511
  </div>
459
- )}
460
-
461
- {/* ====== ИГРОВОЙ ЭКРАН ====== */}
462
- {started && !finished && (
463
- <div style={styles.gmGameLayout}>
464
- <div
465
- style={{
466
- minHeight: 160,
467
- display: "flex",
468
- flexDirection: "column",
469
- justifyContent: "center",
470
- alignItems: "center",
471
- }}
472
- >
473
- {phase === "ready" && (
474
- <>
475
- <h1 style={{ ...styles.gmHeadline1, color: "#ec4c44" }}>GET READY</h1>
476
- <div style={styles.gmHourglass}>⏳</div>
477
- </>
478
- )}
479
- {phase === "memorize" && (
480
- <p style={{ ...styles.gmBodyM, color: "#10b981" }}>
481
- MEMORIZE ({memorizeTime})
482
- </p>
483
- )}
484
- {phase === "guess" && !answered && (
485
- <p style={styles.gmBodyM}>⏳ Time left: {timeLeft}s</p>
486
- )}
487
- </div>
488
512
 
489
- {/* Сетка карточек */}
490
- {phase !== "ready" && (() => {
491
- const gap = isMobile ? 12 : 20;
492
- const innerGutters = 32;
493
- const effective = Math.min(gameCubeSize ?? 1000, squareSize ?? 1000, screenHeight ?? 1000, screenWidth ?? 1000);
494
- const columns = isMobile ? 2 : 3;
495
- const cardSize = Math.max(
496
- 90,
497
- Math.floor((effective - innerGutters - gap * (columns - 1)) / columns)
498
- );
499
-
500
- return (
501
- <div
502
- style={{
503
- ...styles.gmGrid,
504
- gridTemplateColumns: `repeat(${columns}, ${cardSize}px)`,
505
- gridAutoRows: `${cardSize}px`,
506
- gap: `${gap}px`,
507
- justifyContent: "center",
508
- justifyItems: "center",
509
- padding: 8,
510
- boxSizing: "border-box",
511
- }}
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
+ />
590
+ <button
591
+ style={{ ...styles.gmButton, marginLeft: 8 }}
592
+ onClick={checkAnswer}
593
+ disabled={animating}
512
594
  >
513
- {files.map((file, i) => {
514
- const isHidden = hiddenIndex === i && phase === "guess" && !answered;
515
- return (
516
- <div
517
- key={i}
518
- style={{
519
- ...styles.gmCard,
520
- width: `${cardSize}px`,
521
- height: `${cardSize}px`,
522
- padding: isMobile ? 8 : 10,
523
- boxSizing: "border-box",
524
- ...(result === "correct" && hiddenIndex === i ? styles.gmCorrect : {}),
525
- ...(result === "wrong" && hiddenIndex === i ? styles.gmWrong : {}),
526
- }}
527
- >
528
- {!isHidden && (
529
- <img
530
- src={file.src}
531
- alt={file.name}
532
- style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
533
- />
534
- )}
535
- </div>
536
- );
537
- })}
538
- </div>
539
- );
540
- })()}
541
-
542
- {/* Инпут и кнопки */}
543
- <div style={{ marginTop: 16, height: 80 }}>
544
- {phase === "guess" && !answered && (
545
- <div>
546
- <input
547
- type="text"
548
- placeholder="Type the missing word"
549
- value={inputValue}
550
- onChange={(e) => setInputValue(e.target.value)}
551
- style={styles.gmInput}
552
- />
553
- <button
554
- style={{ ...styles.gmButton, marginLeft: 8 }}
555
- onClick={checkAnswer}
556
- disabled={animating}
557
- >
558
- {animating ? "..." : "Check"}
559
- </button>
560
- </div>
561
- )}
562
- {answered && (
563
- <button type="button" style={styles.gmButton} onClick={nextRound}>
564
- Next round
595
+ {animating ? "..." : "Check"}
565
596
  </button>
566
- )}
567
- </div>
597
+ </div>
598
+ )}
599
+ {answered && (
600
+ <button type="button" style={styles.gmButton} onClick={nextRound}>
601
+ Next round
602
+ </button>
603
+ )}
568
604
  </div>
569
- )}
570
- </div>
605
+ </div>
606
+ )}
571
607
  </div>
572
608
  </div>
573
609
  </div>