@mideind/netskrafl-react 1.3.0 → 1.4.0

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/dist/cjs/index.js CHANGED
@@ -23,8 +23,8 @@ const saveAuthSettings = (settings) => {
23
23
  filteredSettings.userId = settings.userId;
24
24
  if (settings.userNick !== undefined)
25
25
  filteredSettings.userNick = settings.userNick;
26
- if (settings.firebaseAPIKey !== undefined)
27
- filteredSettings.firebaseAPIKey = settings.firebaseAPIKey;
26
+ if (settings.firebaseApiKey !== undefined)
27
+ filteredSettings.firebaseApiKey = settings.firebaseApiKey;
28
28
  if (settings.beginner !== undefined)
29
29
  filteredSettings.beginner = settings.beginner;
30
30
  if (settings.fairPlay !== undefined)
@@ -90,7 +90,7 @@ const applyPersistedSettings = (state) => {
90
90
  account: state.account || persisted.account || state.userId, // Use userId as fallback
91
91
  userId: state.userId || persisted.userId || state.userId,
92
92
  userNick: state.userNick || persisted.userNick || state.userNick,
93
- firebaseAPIKey: state.firebaseAPIKey || persisted.firebaseAPIKey || state.firebaseAPIKey,
93
+ firebaseApiKey: state.firebaseApiKey || persisted.firebaseApiKey || state.firebaseApiKey,
94
94
  beginner: (_a = persisted.beginner) !== null && _a !== void 0 ? _a : state.beginner,
95
95
  fairPlay: (_b = persisted.fairPlay) !== null && _b !== void 0 ? _b : state.fairPlay,
96
96
  ready: (_c = persisted.ready) !== null && _c !== void 0 ? _c : state.ready,
@@ -100,8 +100,8 @@ const applyPersistedSettings = (state) => {
100
100
 
101
101
  const DEFAULT_STATE = {
102
102
  projectId: "netskrafl",
103
- firebaseAPIKey: "",
104
- databaseURL: "",
103
+ firebaseApiKey: "",
104
+ databaseUrl: "",
105
105
  firebaseSenderId: "",
106
106
  firebaseAppId: "",
107
107
  measurementId: "",
@@ -27215,12 +27215,12 @@ let database;
27215
27215
  let analytics;
27216
27216
  function initFirebase(state) {
27217
27217
  try {
27218
- const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
27218
+ const { projectId, firebaseApiKey, databaseUrl, firebaseSenderId, firebaseAppId, measurementId } = state;
27219
27219
  const firebaseOptions = {
27220
27220
  projectId,
27221
- apiKey: firebaseAPIKey,
27221
+ apiKey: firebaseApiKey,
27222
27222
  authDomain: `${projectId}.firebaseapp.com`,
27223
- databaseURL,
27223
+ databaseURL: databaseUrl,
27224
27224
  storageBucket: `${projectId}.firebasestorage.app`,
27225
27225
  messagingSenderId: firebaseSenderId,
27226
27226
  appId: firebaseAppId,
@@ -27651,7 +27651,7 @@ const ensureAuthenticated = async (state) => {
27651
27651
  // Update the user's nickname
27652
27652
  state.userNick = result.nickname || state.userNick;
27653
27653
  // Use the server's Firebase API key, if provided
27654
- state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
27654
+ state.firebaseApiKey = result.firebase_api_key || state.firebaseApiKey;
27655
27655
  // Load state flags and preferences
27656
27656
  state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
27657
27657
  state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
@@ -27662,7 +27662,7 @@ const ensureAuthenticated = async (state) => {
27662
27662
  userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
27663
27663
  userId: state.userId,
27664
27664
  userNick: state.userNick,
27665
- firebaseAPIKey: state.firebaseAPIKey,
27665
+ firebaseApiKey: state.firebaseApiKey,
27666
27666
  beginner: state.beginner,
27667
27667
  fairPlay: state.fairPlay,
27668
27668
  ready: state.ready,
@@ -30892,14 +30892,16 @@ const Board = (initialVnode) => {
30892
30892
  }
30893
30893
  return {
30894
30894
  view: (vnode) => {
30895
- const scale = view.boardScale || 1.0;
30895
+ // const scale = view.boardScale || 1.0;
30896
30896
  let attrs = {};
30897
30897
  // Add handlers for pinch zoom functionality
30898
30898
  // Note: resist the temptation to pass zoomIn/zoomOut directly,
30899
30899
  // as that would not bind the 'this' pointer correctly
30900
30900
  addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
30901
+ /*
30901
30902
  if (scale !== 1.0)
30902
- attrs.style = `transform: scale(${scale})`;
30903
+ attrs.style = `transform: scale(${scale})`;
30904
+ */
30903
30905
  return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
30904
30906
  }
30905
30907
  };
@@ -31668,12 +31670,12 @@ const RightColumn = (initialVnode) => {
31668
31670
  */
31669
31671
  const Beginner = (initialVnode) => {
31670
31672
  // Show the board color guide
31671
- const { view } = initialVnode.attrs;
31673
+ const { view, showClose } = initialVnode.attrs;
31672
31674
  const { model, actions } = view;
31673
31675
  const state = model.state;
31674
31676
  return {
31675
31677
  view: () => m(".board-help", { title: ts("Hvernig reitirnir margfalda stigin") }, [
31676
- m(".board-help-close", {
31678
+ showClose ? m(".board-help-close", {
31677
31679
  title: ts("Loka þessari hjálp"),
31678
31680
  onclick: (ev) => {
31679
31681
  // Close the guide and set a preference not to see it again
@@ -31683,7 +31685,7 @@ const Beginner = (initialVnode) => {
31683
31685
  }
31684
31686
  ev.preventDefault();
31685
31687
  }
31686
- }, glyph("remove")),
31688
+ }, glyph("remove")) : null,
31687
31689
  m(".board-colors", [
31688
31690
  m(".board-color[id='triple-word']", ["3 x", m("br"), t("orð")]),
31689
31691
  m(".board-color[id='double-word']", ["2 x", m("br"), t("orð")]),
@@ -31739,7 +31741,7 @@ const GameView = {
31739
31741
  // These elements appear after the game container, since we want
31740
31742
  // them to be above it in the z-order
31741
31743
  m(LeftLogo),
31742
- (state === null || state === void 0 ? void 0 : state.beginner) ? m(Beginner, { view }) : "",
31744
+ (state === null || state === void 0 ? void 0 : state.beginner) ? m(Beginner, { view, showClose: true }) : "",
31743
31745
  m(Info),
31744
31746
  ]);
31745
31747
  }
@@ -32266,6 +32268,8 @@ class View {
32266
32268
  this.dialogStack = [];
32267
32269
  // The current scaling of the board
32268
32270
  this.boardScale = 1.0;
32271
+ // Pending zoom animation timeout (for cancellation across rapid zoom operations)
32272
+ this.zoomAnimationTimeout = null;
32269
32273
  this.selectedTab = "movelist";
32270
32274
  this.actions = actions;
32271
32275
  // Initialize media listeners now that we have the view reference
@@ -32409,13 +32413,17 @@ class View {
32409
32413
  }
32410
32414
  resetScale() {
32411
32415
  // Reset the board scale (zoom) to 100% and the scroll origin to (0, 0)
32416
+ // Cancel any pending zoom animation
32417
+ if (this.zoomAnimationTimeout !== null) {
32418
+ clearTimeout(this.zoomAnimationTimeout);
32419
+ this.zoomAnimationTimeout = null;
32420
+ }
32412
32421
  this.boardScale = 1.0;
32413
32422
  const boardParent = document.getElementById("board-parent");
32414
32423
  const board = boardParent === null || boardParent === void 0 ? void 0 : boardParent.children[0];
32415
32424
  if (board) {
32416
32425
  board.style.transition = 'none';
32417
- board.style.transformOrigin = 'top left';
32418
- board.style.transform = `translate(0, 0) scale(1.0)`;
32426
+ board.style.transform = `translate(0px, 0px) scale(1)`;
32419
32427
  }
32420
32428
  if (boardParent)
32421
32429
  boardParent.scrollTo(0, 0);
@@ -32426,7 +32434,7 @@ class View {
32426
32434
  // Use either the regular game or the riddle (Gáta Dagsins)
32427
32435
  const game = model.game || model.riddle;
32428
32436
  // Update the board scale (zoom)
32429
- function scrollIntoView(sq) {
32437
+ const scrollIntoView = (sq) => {
32430
32438
  // Scroll a square above and to the left of the placed tile into view,
32431
32439
  // with a smooth concurrent zoom and pan animation,
32432
32440
  // taking clamping into account to ensure that the board always fills
@@ -32440,9 +32448,7 @@ class View {
32440
32448
  const row = Math.max(0, vec.row - offset);
32441
32449
  const col = Math.max(0, vec.col - offset);
32442
32450
  const c = coord(row, col);
32443
- // Temporarily set scale to calculate target scroll position
32444
- board.style.transformOrigin = 'top left';
32445
- board.style.transform = `translate(0, 0) scale(1.0)`;
32451
+ // board.style.transform = `translate(0, 0) scale(1.0)`;
32446
32452
  const el = document.getElementById("sq_" + c);
32447
32453
  const elRect = el === null || el === void 0 ? void 0 : el.getBoundingClientRect();
32448
32454
  const boardRect = boardParent.getBoundingClientRect();
@@ -32460,22 +32466,32 @@ class View {
32460
32466
  // the top left corner of the viewport, or as close as possible
32461
32467
  let targetScrollLeft = Math.min(elRect.left - boardRect.left, maxScrollLeft);
32462
32468
  let targetScrollTop = Math.min(elRect.top - boardRect.top, maxScrollTop);
32469
+ // Cancel any pending zoom animation
32470
+ if (this.zoomAnimationTimeout !== null) {
32471
+ clearTimeout(this.zoomAnimationTimeout);
32472
+ this.zoomAnimationTimeout = null;
32473
+ }
32463
32474
  // Now animate both translate (for pan) and scale (for zoom) concurrently
32464
- board.style.transition = 'transform 0.3s ease-in-out';
32465
32475
  // Note: transforms are applied right to left
32466
- board.style.transform =
32467
- `translate(${-targetScrollLeft}px, ${-targetScrollTop}px) scale(${ZOOM_FACTOR})`;
32468
- // When animation completes, commit to actual scroll position and reset translate
32469
- board.addEventListener('transitionend', function handler() {
32470
- // First reset the transform (remove translate, keep scale)
32476
+ board.style.transition = 'transform 0.3s ease-in-out';
32477
+ const translateValue = `${-targetScrollLeft}px, ${-targetScrollTop}px`;
32478
+ const scaleValue = `${ZOOM_FACTOR}`;
32479
+ board.style.transform = `translate(${translateValue}) scale(${scaleValue})`;
32480
+ // Use setTimeout as primary mechanism (transitionend is unreliable in Firefox/Safari)
32481
+ // 350ms = 300ms animation + 50ms buffer
32482
+ this.zoomAnimationTimeout = setTimeout(() => {
32483
+ this.zoomAnimationTimeout = null;
32484
+ // Reset the transform (remove translate, keep scale)
32471
32485
  board.style.transition = 'none';
32472
- board.style.transform = `translate(0, 0) scale(${ZOOM_FACTOR})`;
32473
- // Now set the actual scroll position (already clamped)
32474
- boardParent.scrollTo(targetScrollLeft, targetScrollTop);
32475
- board.removeEventListener('transitionend', handler);
32476
- }, { once: true });
32486
+ board.style.transform = `translate(0px, 0px) scale(${ZOOM_FACTOR})`;
32487
+ // Wait for the browser to process the style changes before setting scroll position
32488
+ // This is needed for Firefox and Safari to properly update the scroll position
32489
+ requestAnimationFrame(() => {
32490
+ boardParent.scrollTo(targetScrollLeft, targetScrollTop);
32491
+ });
32492
+ }, 350);
32477
32493
  }
32478
- }
32494
+ };
32479
32495
  if (!game || ((_a = model.state) === null || _a === void 0 ? void 0 : _a.uiFullscreen) || game.moveInProgress) {
32480
32496
  // No game or we're in full screen mode: always 100% scale
32481
32497
  // Also, as soon as a move is being processed by the server, we zoom out
@@ -36612,27 +36628,25 @@ const SunCorona = {
36612
36628
  const MobileStatus = () => {
36613
36629
  return {
36614
36630
  view: (vnode) => {
36615
- const { riddle, selectedMoves, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
36616
- const { bestPossibleScore, globalBestScore } = riddle;
36631
+ const { view, selectedMoves, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
36632
+ const { riddle } = view.model;
36633
+ if (!riddle)
36634
+ return null;
36635
+ const { bestPossibleScore, globalBestScore, personalBestScore } = riddle;
36617
36636
  // Determine if player achieved best possible score
36618
36637
  const achieved = bestMove !== undefined;
36619
36638
  const celebrate = bestMove && bestMove.word !== "";
36620
- // Get player's current best score
36621
- // If the player achieved the best possible score, it's in bestMove (not selectedMoves)
36622
- const playerBestScore = (bestMove && bestMove.word !== "")
36623
- ? bestMove.score
36624
- : (selectedMoves.length > 0 ? selectedMoves[0].score : 0);
36625
36639
  // Determine current leader score (may be this player or another)
36626
36640
  let leaderScore = 0;
36627
36641
  let isPlayerLeading = false;
36628
36642
  if (globalBestScore && globalBestScore.score > 0) {
36629
36643
  leaderScore = globalBestScore.score;
36630
36644
  // Check if player is leading
36631
- isPlayerLeading = playerBestScore >= globalBestScore.score;
36645
+ isPlayerLeading = personalBestScore >= globalBestScore.score;
36632
36646
  }
36633
36647
  else {
36634
- leaderScore = playerBestScore;
36635
- isPlayerLeading = playerBestScore > 0;
36648
+ leaderScore = personalBestScore;
36649
+ isPlayerLeading = personalBestScore > 0;
36636
36650
  }
36637
36651
  return m(".mobile-status-container", [
36638
36652
  // Current word score (leftmost) - uses RiddleScore component in mobile mode
@@ -36644,12 +36658,12 @@ const MobileStatus = () => {
36644
36658
  }, [
36645
36659
  // Player's best score
36646
36660
  m(".mobile-status-card-item.player-best", [
36647
- m(".mobile-status-label", ts("Þín besta:")),
36648
- m(".mobile-status-score", playerBestScore.toString())
36661
+ m(".mobile-status-label", ts("Þín besta")),
36662
+ m(".mobile-status-score", personalBestScore.toString())
36649
36663
  ]),
36650
36664
  // Current leader score
36651
36665
  m(".mobile-status-card-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
36652
- m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
36666
+ m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Best til þessa")),
36653
36667
  m(".mobile-status-score", leaderScore.toString())
36654
36668
  ]),
36655
36669
  // Chevron indicator (overlaid at bottom center)
@@ -36786,7 +36800,7 @@ const BestPossibleScore = () => {
36786
36800
  }
36787
36801
  else {
36788
36802
  // Someone else achieved it - indicate this
36789
- topLabel = ts("Bestu lögn náð!");
36803
+ topLabel = ts("Besta mögulega lögn\n(er þegar fundin)");
36790
36804
  }
36791
36805
  }
36792
36806
  else {
@@ -36836,7 +36850,7 @@ const PlayerMovesOverlay = () => {
36836
36850
  const allMoveElements = [];
36837
36851
  function scoreDetails(move) {
36838
36852
  if (move.isGlobalBestScore) {
36839
- if (move.word === "" && move.coord === "") {
36853
+ if (move.word === "") {
36840
36854
  // Another player holds the top score
36841
36855
  return [m(GlobalBestScore, { thisPlayer: false, score: move.score })];
36842
36856
  }
@@ -37151,7 +37165,7 @@ const GataDagsinsRightSide = {
37151
37165
  return m(".gatadagsins-right-side-wrapper", riddle ? [
37152
37166
  // Mobile-only status bar (visible on mobile, hidden on desktop)
37153
37167
  m(".gatadagsins-mobile-status", m(MobileStatus, {
37154
- riddle,
37168
+ view,
37155
37169
  selectedMoves,
37156
37170
  bestMove,
37157
37171
  onMoveClick: handleMoveClick,
@@ -37296,14 +37310,14 @@ const StatsModal = () => {
37296
37310
  return [
37297
37311
  // Backdrop
37298
37312
  m(".modal-backdrop-netskrafl", {
37299
- onclick: (e) => { e.preventDefault(); },
37300
- onwheel: (e) => { e.preventDefault(); e.stopPropagation(); },
37301
- ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
37313
+ onclick: (e) => { e.preventDefault(); return false; },
37314
+ onwheel: (e) => { e.preventDefault(); e.stopPropagation(); return false; },
37315
+ ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); return false; }
37302
37316
  }),
37303
37317
  // Modal dialog
37304
37318
  m(".modal-dialog.stats-modal", {
37305
- onwheel: (e) => { e.stopPropagation(); },
37306
- ontouchmove: (e) => { e.stopPropagation(); }
37319
+ onwheel: (e) => { e.stopPropagation(); return false; },
37320
+ ontouchmove: (e) => { e.stopPropagation(); return false; }
37307
37321
  }, [
37308
37322
  m(".modal-content", [
37309
37323
  // Close button in top right
@@ -37436,9 +37450,9 @@ const GataDagsins$1 = () => {
37436
37450
  id: "gatadagsins-background",
37437
37451
  }, [
37438
37452
  // The main content area
37439
- riddle ? m(".gatadagsins-container", [
37453
+ m(".gatadagsins-container", [
37440
37454
  // Main display area with flex layout
37441
- m(".gatadagsins-main", [
37455
+ riddle ? m(".gatadagsins-main", [
37442
37456
  // Board and rack component (left side)
37443
37457
  m(GataDagsinsBoardAndRack, { view }),
37444
37458
  // Right-side component with scores and comparisons
@@ -37447,17 +37461,17 @@ const GataDagsins$1 = () => {
37447
37461
  riddle.askingForBlank
37448
37462
  ? m(BlankDialog, { game: riddle })
37449
37463
  : "",
37450
- ])
37451
- ]) : "",
37452
- // The left margin elements: back button and info/help button
37453
- // These elements appear after the main container for proper z-order
37454
- // m(LeftLogo), // Currently no need for the logo for Gáta Dagsins
37455
- // Show the Beginner component if the user is a beginner
37456
- ((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
37457
- // Custom Info button for GataDagsins that shows help dialog
37458
- m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
37459
- // Help dialog and backdrop
37460
- showHelp ? m(GataDagsinsHelp, { onClose: toggleHelp }) : "",
37464
+ ]) : "",
37465
+ // The left margin elements: back button and info/help button
37466
+ // These elements appear after the main container for proper z-order
37467
+ // m(LeftLogo), // Currently no need for the logo for Gáta Dagsins
37468
+ // Show the Beginner component if the user is a beginner
37469
+ ((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view, showClose: false }) : "",
37470
+ // Custom Info button for GataDagsins that shows help dialog
37471
+ m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
37472
+ // Help dialog and backdrop
37473
+ showHelp ? m(GataDagsinsHelp, { onClose: toggleHelp }) : "",
37474
+ ]),
37461
37475
  // Stats modal and backdrop (mobile only)
37462
37476
  showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
37463
37477
  ]);