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