@mideind/netskrafl-react 1.3.0 → 1.5.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
  };
@@ -31450,7 +31452,6 @@ const Movelist = (initialVnode) => {
31450
31452
  view,
31451
31453
  move,
31452
31454
  info: {
31453
- key: i.toString(),
31454
31455
  leftTotal: leftTotal, rightTotal: rightTotal,
31455
31456
  player: player, co: co, tiles: tiles, score: score
31456
31457
  }
@@ -31668,12 +31669,12 @@ const RightColumn = (initialVnode) => {
31668
31669
  */
31669
31670
  const Beginner = (initialVnode) => {
31670
31671
  // Show the board color guide
31671
- const { view } = initialVnode.attrs;
31672
+ const { view, showClose } = initialVnode.attrs;
31672
31673
  const { model, actions } = view;
31673
31674
  const state = model.state;
31674
31675
  return {
31675
31676
  view: () => m(".board-help", { title: ts("Hvernig reitirnir margfalda stigin") }, [
31676
- m(".board-help-close", {
31677
+ showClose ? m(".board-help-close", {
31677
31678
  title: ts("Loka þessari hjálp"),
31678
31679
  onclick: (ev) => {
31679
31680
  // Close the guide and set a preference not to see it again
@@ -31683,7 +31684,7 @@ const Beginner = (initialVnode) => {
31683
31684
  }
31684
31685
  ev.preventDefault();
31685
31686
  }
31686
- }, glyph("remove")),
31687
+ }, glyph("remove")) : null,
31687
31688
  m(".board-colors", [
31688
31689
  m(".board-color[id='triple-word']", ["3 x", m("br"), t("orð")]),
31689
31690
  m(".board-color[id='double-word']", ["2 x", m("br"), t("orð")]),
@@ -31739,7 +31740,7 @@ const GameView = {
31739
31740
  // These elements appear after the game container, since we want
31740
31741
  // them to be above it in the z-order
31741
31742
  m(LeftLogo),
31742
- (state === null || state === void 0 ? void 0 : state.beginner) ? m(Beginner, { view }) : "",
31743
+ (state === null || state === void 0 ? void 0 : state.beginner) ? m(Beginner, { view, showClose: true }) : "",
31743
31744
  m(Info),
31744
31745
  ]);
31745
31746
  }
@@ -31759,79 +31760,6 @@ const GameView = {
31759
31760
  For further information, see https://github.com/mideind/Netskrafl
31760
31761
 
31761
31762
  */
31762
- const vwReview = (view) => {
31763
- // A review of a finished game
31764
- var _a;
31765
- const model = view.model;
31766
- if (!model.game)
31767
- return undefined;
31768
- const game = model.game;
31769
- let moveIndex = (_a = model.reviewMove) !== null && _a !== void 0 ? _a : 0;
31770
- let bestMoves = model.bestMoves || [];
31771
- function vwRightColumn() {
31772
- // A container for the right-side header and area components
31773
- function vwRightHeading() {
31774
- // The right-side heading on the game screen
31775
- const fairplay = game.fairplay;
31776
- const player = game.player;
31777
- let sc0 = "";
31778
- let sc1 = "";
31779
- if (moveIndex) {
31780
- let s0 = 0;
31781
- let s1 = 0;
31782
- for (let i = 0; i < moveIndex; i++) {
31783
- // Add up the scores until and including this move
31784
- let m = game.moves[i];
31785
- if (i % 2 === 0)
31786
- s0 += m[1][2];
31787
- else
31788
- s1 += m[1][2];
31789
- }
31790
- sc0 = s0.toString();
31791
- sc1 = s1.toString();
31792
- }
31793
- return m(".heading", [
31794
- m(".logowrapper", m(".header-logo", m(m.route.Link, {
31795
- href: "/page",
31796
- class: "backlink"
31797
- }, m(NetskraflLogoOnly)))),
31798
- m(".playerwrapper", [
31799
- m(".leftplayer" + (player === 1 ? ".autoplayercolor" : ".humancolor"), [
31800
- m(".player", m(PlayerName, { view, side: "left" })),
31801
- m(".scorewrapper", m(".scoreleft", sc0)),
31802
- ]),
31803
- m(".rightplayer" + (player === 1 ? ".humancolor" : ".autoplayercolor"), [
31804
- m(".player", m(PlayerName, { view, side: "right" })),
31805
- m(".scorewrapper", m(".scoreright", sc1)),
31806
- ]),
31807
- m(".fairplay", { style: { visibility: fairplay ? "visible" : "hidden" } }, m("span.fairplay-btn.large", { title: ts("Skraflað án hjálpartækja") }))
31808
- ])
31809
- ]);
31810
- }
31811
- function vwRightArea() {
31812
- // A container for the list of best possible moves
31813
- return m(".right-area", vwBestMoves(view, moveIndex, bestMoves));
31814
- }
31815
- return m(".rightcol", [vwRightHeading(), vwRightArea()]);
31816
- }
31817
- let r = [];
31818
- if (game) {
31819
- // Create a list of major elements that we're showing
31820
- r.push(vwRightColumn());
31821
- r.push(m(BoardReview, { view, moveIndex }));
31822
- if (model.reviewMove !== null && moveIndex === 0) {
31823
- // Only show the stats overlay if moveIndex is 0
31824
- const n = vwStatsReview(view);
31825
- n && r.push(n);
31826
- }
31827
- }
31828
- return m("div", // Removing this div messes up Mithril
31829
- [
31830
- m(".game-container", r),
31831
- m(LeftLogo), // Button to go back to main screen
31832
- m(Info) // Help button
31833
- ]);
31834
- };
31835
31763
  const vwBestMoves = (view, moveIndex, bestMoves) => {
31836
31764
  // List of best moves, in a game review
31837
31765
  const model = view.model;
@@ -31886,7 +31814,7 @@ const vwBestMoves = (view, moveIndex, bestMoves) => {
31886
31814
  // and thus cannot be confused with the above abbreviations)
31887
31815
  wrdclass = "othermove";
31888
31816
  if (tiles == "--")
31889
- dispText = ts("Stafaleif: (engin)");
31817
+ dispText = ts("Stafaleif: engin");
31890
31818
  else
31891
31819
  dispText = [ts("Stafaleif: "), m("i.upper", tiles)];
31892
31820
  }
@@ -31903,8 +31831,8 @@ const vwBestMoves = (view, moveIndex, bestMoves) => {
31903
31831
  if (!game || !moveIndex || moveIndex > game.moves.length)
31904
31832
  return r;
31905
31833
  // Prepend a header that describes the move being reviewed
31906
- const m = game.moves[moveIndex - 1];
31907
- const [co, tiles, score] = m[1];
31834
+ const move = game.moves[moveIndex - 1];
31835
+ const [co, tiles, score] = move[1];
31908
31836
  r.push(bestHeader(co, tiles, score));
31909
31837
  const mlist = bestMoves;
31910
31838
  for (let i = 0; i < mlist.length; i++) {
@@ -31916,7 +31844,7 @@ const vwBestMoves = (view, moveIndex, bestMoves) => {
31916
31844
  }
31917
31845
  return r;
31918
31846
  }
31919
- return m(".movelist-container", [m(".movelist.bestmoves", bestMoveList())]);
31847
+ return m(".movelist-container", m(".movelist.bestmoves", bestMoveList()));
31920
31848
  };
31921
31849
  const vwBestMove = (view, moveIndex, bestMoveIndex, move, info) => {
31922
31850
  // Displays a move in a list of best available moves
@@ -32215,6 +32143,81 @@ const vwButtonsReview = (view, moveIndex) => {
32215
32143
  r.push(n);
32216
32144
  return r;
32217
32145
  };
32146
+ const Review = (initialVnode) => {
32147
+ // A review of a finished game
32148
+ const { view } = initialVnode.attrs;
32149
+ function vwRightColumn(game, moveIndex, bestMoves) {
32150
+ // A container for the right-side header and area components
32151
+ function vwRightHeading() {
32152
+ // The right-side heading on the game screen
32153
+ const fairplay = game.fairplay;
32154
+ const player = game.player;
32155
+ let sc0 = "";
32156
+ let sc1 = "";
32157
+ if (moveIndex) {
32158
+ let s0 = 0;
32159
+ let s1 = 0;
32160
+ for (let i = 0; i < moveIndex; i++) {
32161
+ // Add up the scores until and including this move
32162
+ let m = game.moves[i];
32163
+ if (i % 2 === 0)
32164
+ s0 += m[1][2];
32165
+ else
32166
+ s1 += m[1][2];
32167
+ }
32168
+ sc0 = s0.toString();
32169
+ sc1 = s1.toString();
32170
+ }
32171
+ return m(".heading", [
32172
+ m(".logowrapper", m(".header-logo", m(m.route.Link, {
32173
+ href: "/page",
32174
+ class: "backlink"
32175
+ }, m(NetskraflLogoOnly)))),
32176
+ m(".playerwrapper", [
32177
+ m(".leftplayer" + (player === 1 ? ".autoplayercolor" : ".humancolor"), [
32178
+ m(".player", m(PlayerName, { view, side: "left" })),
32179
+ m(".scorewrapper", m(".scoreleft", sc0)),
32180
+ ]),
32181
+ m(".rightplayer" + (player === 1 ? ".humancolor" : ".autoplayercolor"), [
32182
+ m(".player", m(PlayerName, { view, side: "right" })),
32183
+ m(".scorewrapper", m(".scoreright", sc1)),
32184
+ ]),
32185
+ m(".fairplay", { style: { visibility: fairplay ? "visible" : "hidden" } }, m("span.fairplay-btn.large", { title: ts("Skraflað án hjálpartækja") }))
32186
+ ])
32187
+ ]);
32188
+ }
32189
+ function vwRightArea() {
32190
+ // A container for the list of best possible moves
32191
+ return m(".right-area", vwBestMoves(view, moveIndex, bestMoves));
32192
+ }
32193
+ return m(".rightcol", [vwRightHeading(), vwRightArea()]);
32194
+ }
32195
+ return {
32196
+ view: () => {
32197
+ var _a;
32198
+ let r = [];
32199
+ const { model } = view;
32200
+ const { game } = model;
32201
+ if (game) {
32202
+ // Create a list of major elements that we're showing
32203
+ const moveIndex = (_a = model.reviewMove) !== null && _a !== void 0 ? _a : 0;
32204
+ const bestMoves = model.bestMoves || [];
32205
+ r.push(vwRightColumn(game, moveIndex, bestMoves));
32206
+ r.push(m(BoardReview, { view, moveIndex }));
32207
+ if (model.reviewMove !== null && moveIndex === 0) {
32208
+ // Only show the stats overlay if moveIndex is 0
32209
+ const n = vwStatsReview(view);
32210
+ n && r.push(n);
32211
+ }
32212
+ }
32213
+ return m("div", [
32214
+ m(".game-container", r),
32215
+ m(LeftLogo), // Button to go back to main screen
32216
+ m(Info) // Help button
32217
+ ]);
32218
+ }
32219
+ };
32220
+ };
32218
32221
  const BoardReview = {
32219
32222
  // The board area within a game review screen
32220
32223
  view: (vnode) => {
@@ -32266,6 +32269,8 @@ class View {
32266
32269
  this.dialogStack = [];
32267
32270
  // The current scaling of the board
32268
32271
  this.boardScale = 1.0;
32272
+ // Pending zoom animation timeout (for cancellation across rapid zoom operations)
32273
+ this.zoomAnimationTimeout = null;
32269
32274
  this.selectedTab = "movelist";
32270
32275
  this.actions = actions;
32271
32276
  // Initialize media listeners now that we have the view reference
@@ -32292,8 +32297,7 @@ class View {
32292
32297
  views.push(m(GameView, { key: "game", view: this }));
32293
32298
  break;
32294
32299
  case "review":
32295
- const n = vwReview(this);
32296
- n && views.push(n);
32300
+ views.push(m(Review, { key: "review", view: this }));
32297
32301
  break;
32298
32302
  case "thanks":
32299
32303
  // Display a thank-you dialog on top of the normal main screen
@@ -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
@@ -35859,6 +35875,8 @@ class Actions {
35859
35875
  if (state && !state.uiFullscreen) {
35860
35876
  state.uiFullscreen = true;
35861
35877
  if (view) {
35878
+ // Reset zoom when switching to fullscreen (desktop always uses scale 1)
35879
+ view.resetScale();
35862
35880
  view.notifyMediaChange();
35863
35881
  }
35864
35882
  m.redraw();
@@ -36199,6 +36217,8 @@ class Actions {
36199
36217
  // Listen to user stats (if user is logged in)
36200
36218
  if (state === null || state === void 0 ? void 0 : state.userId) {
36201
36219
  attachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`, (json, firstAttach) => this.onUserStatsUpdate(json, firstAttach));
36220
+ // Listen to personal best move (entire achievement object)
36221
+ attachFirebaseListener(basePath + `achievements/${state.userId}`, (json, firstAttach) => this.onPersonalBestScoreUpdate(json, firstAttach));
36202
36222
  }
36203
36223
  }
36204
36224
  detachListenerFromRiddle(date, locale) {
@@ -36211,6 +36231,7 @@ class Actions {
36211
36231
  detachFirebaseListener(basePath + "leaders");
36212
36232
  if (state === null || state === void 0 ? void 0 : state.userId) {
36213
36233
  detachFirebaseListener(`gatadagsins/users/${locale}/${state.userId}/stats`);
36234
+ detachFirebaseListener(basePath + `achievements/${state.userId}`);
36214
36235
  }
36215
36236
  }
36216
36237
  onRiddleGlobalScoreUpdate(json, firstAttach) {
@@ -36261,6 +36282,35 @@ class Actions {
36261
36282
  }
36262
36283
  m.redraw();
36263
36284
  }
36285
+ onPersonalBestScoreUpdate(json, firstAttach) {
36286
+ const { riddle } = this.model;
36287
+ if (!riddle || !json)
36288
+ return;
36289
+ // Extract the full move from Firebase
36290
+ const score = json.score || 0;
36291
+ const word = json.word || "";
36292
+ const coord = json.coord || "";
36293
+ const timestamp = json.timestamp || new Date().toISOString();
36294
+ // Only proceed if we didn't already have this score or better
36295
+ if (score <= riddle.personalBestScore || !word || !coord)
36296
+ return;
36297
+ riddle.personalBestScore = score;
36298
+ // Check if this move is already in playerMoves
36299
+ // (this is a safety precaution; normally the
36300
+ // move should not be there already)
36301
+ const moveExists = riddle.playerMoves.some(m => m.word === word && m.coord === coord && m.score === score);
36302
+ // If not already present, add it to playerMoves
36303
+ // This enables clicking on it to see it on the board
36304
+ if (!moveExists) {
36305
+ riddle.playerMoves.push({
36306
+ word,
36307
+ score,
36308
+ coord,
36309
+ timestamp
36310
+ });
36311
+ }
36312
+ m.redraw();
36313
+ }
36264
36314
  async fetchRiddle(date, locale) {
36265
36315
  // Create the game via model
36266
36316
  if (!this.model)
@@ -36612,27 +36662,24 @@ const SunCorona = {
36612
36662
  const MobileStatus = () => {
36613
36663
  return {
36614
36664
  view: (vnode) => {
36615
- const { riddle, selectedMoves, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
36616
- const { bestPossibleScore, globalBestScore } = riddle;
36665
+ const { view, selectedMoves, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
36666
+ const { riddle } = view.model;
36667
+ if (!riddle)
36668
+ return null;
36669
+ const { bestPossibleScore, globalBestScore, personalBestScore } = riddle;
36617
36670
  // Determine if player achieved best possible score
36618
- const achieved = bestMove !== undefined;
36619
36671
  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
36672
  // Determine current leader score (may be this player or another)
36626
36673
  let leaderScore = 0;
36627
36674
  let isPlayerLeading = false;
36628
36675
  if (globalBestScore && globalBestScore.score > 0) {
36629
36676
  leaderScore = globalBestScore.score;
36630
36677
  // Check if player is leading
36631
- isPlayerLeading = playerBestScore >= globalBestScore.score;
36678
+ isPlayerLeading = personalBestScore >= globalBestScore.score;
36632
36679
  }
36633
36680
  else {
36634
- leaderScore = playerBestScore;
36635
- isPlayerLeading = playerBestScore > 0;
36681
+ leaderScore = personalBestScore;
36682
+ isPlayerLeading = personalBestScore > 0;
36636
36683
  }
36637
36684
  return m(".mobile-status-container", [
36638
36685
  // Current word score (leftmost) - uses RiddleScore component in mobile mode
@@ -36644,12 +36691,12 @@ const MobileStatus = () => {
36644
36691
  }, [
36645
36692
  // Player's best score
36646
36693
  m(".mobile-status-card-item.player-best", [
36647
- m(".mobile-status-label", ts("Þín besta:")),
36648
- m(".mobile-status-score", playerBestScore.toString())
36694
+ m(".mobile-status-label", ts("Þín besta")),
36695
+ m(".mobile-status-score", personalBestScore.toString())
36649
36696
  ]),
36650
36697
  // Current leader score
36651
36698
  m(".mobile-status-card-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
36652
- m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
36699
+ m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Best til þessa")),
36653
36700
  m(".mobile-status-score", leaderScore.toString())
36654
36701
  ]),
36655
36702
  // Chevron indicator (overlaid at bottom center)
@@ -36657,8 +36704,7 @@ const MobileStatus = () => {
36657
36704
  ]),
36658
36705
  // Best possible score
36659
36706
  m(".mobile-status-item.right.best-possible"
36660
- + (celebrate ? ".celebrate" : "")
36661
- + (achieved ? ".achieved" : ""), {
36707
+ + (celebrate ? ".celebrate" : ""), {
36662
36708
  onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
36663
36709
  }, [
36664
36710
  // Wrapper for score and corona to position them together
@@ -36779,25 +36825,17 @@ const BestPossibleScore = () => {
36779
36825
  const { score, bestMove, onMoveClick } = vnode.attrs;
36780
36826
  // Determine the label based on achievement status
36781
36827
  let topLabel;
36782
- if (bestMove !== undefined) {
36783
- if (bestMove.word) {
36784
- // Current player achieved it - show their word
36785
- topLabel = removeBlankMarkers(bestMove.word);
36786
- }
36787
- else {
36788
- // Someone else achieved it - indicate this
36789
- topLabel = ts("Bestu lögn náð!");
36790
- }
36828
+ if (bestMove !== undefined && bestMove.word) {
36829
+ // Current player achieved it - show their word
36830
+ topLabel = removeBlankMarkers(bestMove.word);
36791
36831
  }
36792
36832
  else {
36793
36833
  // Not achieved yet - show default label
36794
36834
  topLabel = ts("Besta mögulega lögn");
36795
36835
  }
36796
- const achieved = bestMove !== undefined;
36797
36836
  const celebrate = bestMove && bestMove.word !== "";
36798
36837
  return m(".thermometer-best-score"
36799
- + (celebrate ? ".celebrate" : "")
36800
- + (achieved ? ".achieved" : ""), m(".thermometer-best-score-container", {
36838
+ + (celebrate ? ".celebrate" : ""), m(".thermometer-best-score-container", {
36801
36839
  onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
36802
36840
  }, [
36803
36841
  // Sun corona behind the circle when celebrating
@@ -36836,7 +36874,7 @@ const PlayerMovesOverlay = () => {
36836
36874
  const allMoveElements = [];
36837
36875
  function scoreDetails(move) {
36838
36876
  if (move.isGlobalBestScore) {
36839
- if (move.word === "" && move.coord === "") {
36877
+ if (move.word === "") {
36840
36878
  // Another player holds the top score
36841
36879
  return [m(GlobalBestScore, { thisPlayer: false, score: move.score })];
36842
36880
  }
@@ -37033,7 +37071,10 @@ const LeaderboardView = {
37033
37071
  m(".leaderboard-header", [
37034
37072
  m(".leaderboard-title", formatDate(date)),
37035
37073
  ]),
37036
- m(".leaderboard-list", leaderboard.map((entry, index) => {
37074
+ m(".leaderboard-list", {
37075
+ // Allow touch scrolling but prevent events from bubbling to backdrop
37076
+ ontouchmove: (e) => { e.stopPropagation(); }
37077
+ }, leaderboard.map((entry, index) => {
37037
37078
  const rank = index + 1;
37038
37079
  const isCurrentUser = entry.userId === currentUserId;
37039
37080
  const medal = getMedalIcon(rank);
@@ -37041,7 +37082,9 @@ const LeaderboardView = {
37041
37082
  m(".entry-rank", [
37042
37083
  medal ? m("span.medal", medal) : m("span.rank-number", rank.toString())
37043
37084
  ]),
37044
- m(".entry-name", isCurrentUser ? ts("Þú") : entry.displayName),
37085
+ m(".entry-name", isCurrentUser
37086
+ ? [m("span.entry-star", glyph("star")), ts("Þú")]
37087
+ : entry.displayName),
37045
37088
  m(".entry-score", entry.score.toString())
37046
37089
  ]);
37047
37090
  }))
@@ -37142,7 +37185,7 @@ const GataDagsinsRightSide = {
37142
37185
  view: (vnode) => {
37143
37186
  const { view, selectedMoves, bestMove, onStatsClick } = vnode.attrs;
37144
37187
  const { riddle } = view.model;
37145
- const handleMoveClick = (word, coord) => {
37188
+ const onMoveClick = (word, coord) => {
37146
37189
  if (riddle && word && coord) {
37147
37190
  // Recreate the word on the board
37148
37191
  riddle.recreateWordOnBoard(word, coord);
@@ -37151,18 +37194,18 @@ const GataDagsinsRightSide = {
37151
37194
  return m(".gatadagsins-right-side-wrapper", riddle ? [
37152
37195
  // Mobile-only status bar (visible on mobile, hidden on desktop)
37153
37196
  m(".gatadagsins-mobile-status", m(MobileStatus, {
37154
- riddle,
37197
+ view,
37155
37198
  selectedMoves,
37156
37199
  bestMove,
37157
- onMoveClick: handleMoveClick,
37158
- onStatsClick
37200
+ onMoveClick,
37201
+ onStatsClick,
37159
37202
  })),
37160
37203
  // Desktop-only tabbed view (hidden on mobile, visible on desktop)
37161
37204
  m(".gatadagsins-thermometer-column", m(RightSideTabs, {
37162
37205
  view,
37163
37206
  selectedMoves,
37164
37207
  bestMove,
37165
- onMoveClick: handleMoveClick
37208
+ onMoveClick,
37166
37209
  })),
37167
37210
  ] : null);
37168
37211
  }
@@ -37193,14 +37236,13 @@ const GataDagsinsHelp = {
37193
37236
  ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
37194
37237
  }),
37195
37238
  m(".modal-dialog.gatadagsins-help", m(".modal-content", [
37196
- // Header with close button
37197
- m(".modal-header", [
37198
- m("h2", "Um Gátu dagsins"),
37199
- m("button.close", {
37200
- onclick: closeHelp,
37201
- "aria-label": "Loka"
37202
- }, m("span", { "aria-hidden": "true" }, "×"))
37203
- ]),
37239
+ // Close button (positioned absolutely in top-right corner)
37240
+ m("button.close", {
37241
+ onclick: closeHelp,
37242
+ "aria-label": "Loka"
37243
+ }, glyph("remove")),
37244
+ // Header
37245
+ m(".modal-header", m("h2", "Um Gátu dagsins")),
37204
37246
  // Body with help content
37205
37247
  m(".modal-body", [
37206
37248
  m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir að finna " +
@@ -37219,8 +37261,20 @@ const GataDagsinsHelp = {
37219
37261
  m("ul", [
37220
37262
  m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
37221
37263
  m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
37222
- m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
37223
- m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
37264
+ m("li", [
37265
+ "Sumir reitir á borðinu ",
37266
+ m("span.help-bonus-square.double-letter"),
37267
+ "tvöfalda eða ",
37268
+ m("span.help-bonus-square.triple-letter"),
37269
+ "þrefalda stafagildið"
37270
+ ]),
37271
+ m("li", [
37272
+ "Sumir reitir ",
37273
+ m("span.help-bonus-square.double-word"),
37274
+ "tvöfalda eða ",
37275
+ m("span.help-bonus-square.triple-word"),
37276
+ "þrefalda heildarorðagildið"
37277
+ ]),
37224
37278
  ]),
37225
37279
  m("h3", "Hitamælir"),
37226
37280
  m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
@@ -37296,14 +37350,14 @@ const StatsModal = () => {
37296
37350
  return [
37297
37351
  // Backdrop
37298
37352
  m(".modal-backdrop-netskrafl", {
37299
- onclick: (e) => { e.preventDefault(); },
37300
- onwheel: (e) => { e.preventDefault(); e.stopPropagation(); },
37301
- ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
37353
+ onclick: (e) => { e.preventDefault(); return false; },
37354
+ onwheel: (e) => { e.preventDefault(); e.stopPropagation(); return false; },
37355
+ ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); return false; }
37302
37356
  }),
37303
37357
  // Modal dialog
37304
37358
  m(".modal-dialog.stats-modal", {
37305
- onwheel: (e) => { e.stopPropagation(); },
37306
- ontouchmove: (e) => { e.stopPropagation(); }
37359
+ onwheel: (e) => { e.stopPropagation(); return false; },
37360
+ ontouchmove: (e) => { e.stopPropagation(); return false; }
37307
37361
  }, [
37308
37362
  m(".modal-content", [
37309
37363
  // Close button in top right
@@ -37436,9 +37490,9 @@ const GataDagsins$1 = () => {
37436
37490
  id: "gatadagsins-background",
37437
37491
  }, [
37438
37492
  // The main content area
37439
- riddle ? m(".gatadagsins-container", [
37493
+ m(".gatadagsins-container", [
37440
37494
  // Main display area with flex layout
37441
- m(".gatadagsins-main", [
37495
+ riddle ? m(".gatadagsins-main", [
37442
37496
  // Board and rack component (left side)
37443
37497
  m(GataDagsinsBoardAndRack, { view }),
37444
37498
  // Right-side component with scores and comparisons
@@ -37447,17 +37501,17 @@ const GataDagsins$1 = () => {
37447
37501
  riddle.askingForBlank
37448
37502
  ? m(BlankDialog, { game: riddle })
37449
37503
  : "",
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 }) : "",
37504
+ ]) : "",
37505
+ // The left margin elements: back button and info/help button
37506
+ // These elements appear after the main container for proper z-order
37507
+ // m(LeftLogo), // Currently no need for the logo for Gáta Dagsins
37508
+ // Show the Beginner component if the user is a beginner
37509
+ ((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view, showClose: false }) : "",
37510
+ // Custom Info button for GataDagsins that shows help dialog
37511
+ m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
37512
+ // Help dialog and backdrop
37513
+ showHelp ? m(GataDagsinsHelp, { onClose: toggleHelp }) : "",
37514
+ ]),
37461
37515
  // Stats modal and backdrop (mobile only)
37462
37516
  showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
37463
37517
  ]);