@mideind/netskrafl-react 1.1.0 → 1.2.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
@@ -29206,7 +29206,7 @@ const BestDisplay = () => {
29206
29206
  // Make sure blank tiles get a different color
29207
29207
  for (let i = 0; i < bw.length; i++)
29208
29208
  if (bw[i] == '?') {
29209
- s.push(m("span.blanktile", bw[i + 1]));
29209
+ s.push(m("span.netskrafl-blanktile", bw[i + 1]));
29210
29210
  i += 1;
29211
29211
  }
29212
29212
  else
@@ -30002,7 +30002,7 @@ const BlankDialog = () => {
30002
30002
  onclick: (ev) => { game.placeBlank(letter); ev.preventDefault(); },
30003
30003
  onmouseover: buttonOver,
30004
30004
  onmouseout: buttonOut
30005
- }, m(".blank-choice.tile.racktile", letter)));
30005
+ }, m(".blank-choice.netskrafl-tile.netskrafl-racktile", letter)));
30006
30006
  len--;
30007
30007
  }
30008
30008
  r.push(m("tr", c));
@@ -30097,25 +30097,26 @@ const Bag = () => {
30097
30097
  For further information, see https://github.com/mideind/Netskrafl
30098
30098
 
30099
30099
  */
30100
- function makeButton(cls, disabled, onclick, title, children, id) {
30100
+ function makeButton(cls, disabled, onclick, title, children, visible, id) {
30101
30101
  // Create a button element, wrapping the disabling logic
30102
30102
  // and other boilerplate
30103
- const attr = {
30103
+ const attrs = {
30104
30104
  onmouseout: buttonOut,
30105
30105
  onmouseover: buttonOver,
30106
+ style: { visibility: visible === false ? "hidden" : "visible" },
30106
30107
  };
30107
30108
  if (title)
30108
- attr.title = title;
30109
+ attrs.title = title;
30109
30110
  if (id)
30110
- attr.id = id;
30111
+ attrs.id = id;
30111
30112
  if (disabled)
30112
- attr.onclick = (ev) => ev.preventDefault();
30113
+ attrs.onclick = (ev) => ev.preventDefault();
30113
30114
  else
30114
- attr.onclick = (ev) => {
30115
+ attrs.onclick = (ev) => {
30115
30116
  onclick && onclick();
30116
30117
  ev.preventDefault();
30117
30118
  };
30118
- return m("." + cls + (disabled ? ".disabled" : ""), attr, children // children may be omitted
30119
+ return m("." + cls + (disabled ? ".disabled" : ""), attrs, children // children may be omitted
30119
30120
  );
30120
30121
  }
30121
30122
  const Score = {
@@ -30137,20 +30138,20 @@ const Score = {
30137
30138
  const RecallButton = {
30138
30139
  view: (vnode) => {
30139
30140
  // Create a tile recall button
30140
- const { view, game, disabled } = vnode.attrs;
30141
+ const { view, game, disabled, visible = true } = vnode.attrs;
30141
30142
  if (!game)
30142
30143
  return undefined;
30143
- return makeButton("recallbtn", !!disabled, () => { game.resetRack(); view.updateScale(); }, ts("Færa stafi aftur í rekka"), glyph("down-arrow"));
30144
+ return makeButton("recallbtn", !!disabled, () => { game.resetRack(); view.updateScale(); }, ts("Færa stafi aftur í rekka"), glyph("down-arrow"), visible);
30144
30145
  }
30145
30146
  };
30146
30147
  const ScrambleButton = {
30147
30148
  view: (vnode) => {
30148
30149
  // Create a tile scramble button
30149
- const { game, disabled } = vnode.attrs;
30150
+ const { game, disabled, visible = true } = vnode.attrs;
30150
30151
  if (!game)
30151
30152
  return undefined;
30152
30153
  return makeButton("scramblebtn", !!disabled, () => game.rescrambleRack(), // Note: plain game.rescrambleRack doesn't work here
30153
- ts("Stokka upp rekka"), glyph("random"));
30154
+ ts("Stokka upp rekka"), glyph("random"), visible);
30154
30155
  }
30155
30156
  };
30156
30157
  const ButtonsRecallScramble = {
@@ -30166,9 +30167,9 @@ const ButtonsRecallScramble = {
30166
30167
  if (s.showRecall && !s.showingDialog)
30167
30168
  // Show a 'Recall tiles' button
30168
30169
  return m(RecallButton, { view, game });
30169
- if (s.showScramble && !s.showingDialog)
30170
- return m(ScrambleButton, { view, game });
30171
- return undefined;
30170
+ const visible = s.showScramble && !s.tilesPlaced;
30171
+ const disabled = s.showingDialog;
30172
+ return m(ScrambleButton, { view, game, visible, disabled });
30172
30173
  }
30173
30174
  };
30174
30175
  const Buttons = {
@@ -30245,7 +30246,7 @@ const Buttons = {
30245
30246
  }
30246
30247
  };
30247
30248
  }
30248
- r.push(makeButton(classes.join("."), s.showingDialog, action, text, legend, "move-mobile"));
30249
+ r.push(makeButton(classes.join("."), s.showingDialog, action, text, legend, true, "move-mobile"));
30249
30250
  }
30250
30251
  if (s.showForceResignMobile) {
30251
30252
  // Force resignation button (only shown on mobile,
@@ -30264,7 +30265,7 @@ const Buttons = {
30264
30265
  }
30265
30266
  if (s.showExchange) {
30266
30267
  // Exchange tiles from the rack
30267
- const disabled = (s.tilesPlaced || s.showingDialog) && !s.exchangeAllowed;
30268
+ const disabled = !s.exchangeAllowed || s.tilesPlaced || s.showingDialog;
30268
30269
  r.push(makeButton("submitexchange", disabled, () => game.submitExchange(), // Note: plain game.submitExchange doesn't work here
30269
30270
  ts("Skipta stöfum"), glyph("refresh")));
30270
30271
  }
@@ -30561,24 +30562,24 @@ const Tile = (initialVnode) => {
30561
30562
  const isRackTile = coord[0] === 'R';
30562
30563
  // A single tile, on the board or in the rack
30563
30564
  const t = game.tiles[coord];
30564
- let classes = [".tile"];
30565
+ let classes = [".netskrafl-tile"];
30565
30566
  let attrs = {};
30566
30567
  if (t.tile === '?')
30567
- classes.push("blanktile");
30568
+ classes.push("netskrafl-blanktile");
30568
30569
  if (EXTRA_WIDE_LETTERS.includes(t.letter))
30569
30570
  // Extra wide letter: handle specially
30570
- classes.push("extra-wide");
30571
+ classes.push("netskrafl-extra-wide");
30571
30572
  else if (WIDE_LETTERS.includes(t.letter))
30572
30573
  // Wide letter: handle specially
30573
- classes.push("wide");
30574
+ classes.push("netskrafl-wide");
30574
30575
  if (isRackTile || t.draggable) {
30575
30576
  // Rack tile, or at least a draggable one
30576
- classes.push(opponent ? "freshtile" : "racktile");
30577
+ classes.push(opponent ? "netskrafl-freshtile" : "netskrafl-racktile");
30577
30578
  if (isRackTile && game.showingDialog === "exchange") {
30578
30579
  // Rack tile, and we're showing the exchange dialog
30579
30580
  if (t.xchg)
30580
30581
  // Chosen as an exchange tile
30581
- classes.push("xchgsel");
30582
+ classes.push("netskrafl-xchgsel");
30582
30583
  // Exchange dialog is live: add a click handler for the
30583
30584
  // exchange state
30584
30585
  attrs.onclick = (ev) => {
@@ -30591,7 +30592,7 @@ const Tile = (initialVnode) => {
30591
30592
  else if (t.freshtile) {
30592
30593
  // A fresh tile on the board that has
30593
30594
  // just been played by the opponent
30594
- classes.push("freshtile");
30595
+ classes.push("netskrafl-freshtile");
30595
30596
  }
30596
30597
  if (t.index) {
30597
30598
  // Make fresh or highlighted tiles appear sequentally by animation
@@ -32197,11 +32198,11 @@ const vwButtonsReview = (view, moveIndex) => {
32197
32198
  () => {
32198
32199
  // Navigate to previous moveIndex
32199
32200
  model.loadBestMoves(moveIndex ? moveIndex - 1 : 0);
32200
- }, "Sjá fyrri leik", m("span", { id: "nav-prev-visible" }, [glyph("chevron-left"), " Fyrri"]), "navprev"));
32201
+ }, "Sjá fyrri leik", m("span", { id: "nav-prev-visible" }, [glyph("chevron-left"), " Fyrri"]), true, "navprev"));
32201
32202
  r.push(makeButton("navbtn", (!moveIndex) || (moveIndex >= numMoves), () => {
32202
32203
  // Navigate to next moveIndex
32203
32204
  model.loadBestMoves(moveIndex + 1);
32204
- }, "Sjá næsta leik", m("span", { id: "nav-next-visible" }, ["Næsti ", glyph("chevron-right")]), "navnext"));
32205
+ }, "Sjá næsta leik", m("span", { id: "nav-next-visible" }, ["Næsti ", glyph("chevron-right")]), true, "navnext"));
32205
32206
  // Show the score difference between an actual moveIndex and
32206
32207
  // a particular moveIndex on the best moveIndex list
32207
32208
  if (model.highlightedMove !== null) {
@@ -32411,39 +32412,68 @@ class View {
32411
32412
  this.boardScale = 1.0;
32412
32413
  const boardParent = document.getElementById("board-parent");
32413
32414
  const board = boardParent === null || boardParent === void 0 ? void 0 : boardParent.children[0];
32414
- if (board)
32415
- board.setAttribute("style", "transform: scale(1.0)");
32415
+ if (board) {
32416
+ board.style.transition = 'none';
32417
+ board.style.transformOrigin = 'top left';
32418
+ board.style.transform = `translate(0, 0) scale(1.0)`;
32419
+ }
32416
32420
  if (boardParent)
32417
32421
  boardParent.scrollTo(0, 0);
32418
32422
  }
32419
32423
  updateScale() {
32420
32424
  var _a;
32421
32425
  const model = this.model;
32422
- const game = model.game;
32426
+ // Use either the regular game or the riddle (Gáta Dagsins)
32427
+ const game = model.game || model.riddle;
32423
32428
  // Update the board scale (zoom)
32424
32429
  function scrollIntoView(sq) {
32425
- // Scroll a square above and to the left of the placed tile into view
32430
+ // Scroll a square above and to the left of the placed tile into view,
32431
+ // with a smooth concurrent zoom and pan animation,
32432
+ // taking clamping into account to ensure that the board always fills
32433
+ // the viewport.
32434
+ const boardParent = document.getElementById("board-parent");
32435
+ const board = boardParent === null || boardParent === void 0 ? void 0 : boardParent.children[0];
32436
+ if (!board || !boardParent)
32437
+ return;
32426
32438
  const offset = 3;
32427
32439
  const vec = toVector(sq);
32428
32440
  const row = Math.max(0, vec.row - offset);
32429
32441
  const col = Math.max(0, vec.col - offset);
32430
32442
  const c = coord(row, col);
32431
- const boardParent = document.getElementById("board-parent");
32432
- const board = boardParent === null || boardParent === void 0 ? void 0 : boardParent.children[0];
32433
- // The following seems to be needed to ensure that
32434
- // the transform and hence the size of the board has been
32435
- // updated in the browser, before calculating the client rects
32436
- if (board)
32437
- board.setAttribute("style", `transform: scale(${ZOOM_FACTOR})`);
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)`;
32438
32446
  const el = document.getElementById("sq_" + c);
32439
32447
  const elRect = el === null || el === void 0 ? void 0 : el.getBoundingClientRect();
32440
- const boardRect = boardParent === null || boardParent === void 0 ? void 0 : boardParent.getBoundingClientRect();
32441
- if (boardParent && elRect && boardRect) {
32442
- boardParent.scrollTo({
32443
- left: elRect.left - boardRect.left,
32444
- top: elRect.top - boardRect.top,
32445
- behavior: "smooth"
32446
- });
32448
+ const boardRect = boardParent.getBoundingClientRect();
32449
+ if (elRect && boardRect) {
32450
+ // Get the dimensions of the scrollable area
32451
+ const viewportWidth = boardParent.clientWidth;
32452
+ const viewportHeight = boardParent.clientHeight;
32453
+ // The offsetWidth/Height include borders, so we use those
32454
+ const scaledBoardWidth = board.offsetWidth * ZOOM_FACTOR;
32455
+ const scaledBoardHeight = board.offsetHeight * ZOOM_FACTOR;
32456
+ // Calculate maximum scroll positions (board edges)
32457
+ const maxScrollLeft = scaledBoardWidth - viewportWidth;
32458
+ const maxScrollTop = scaledBoardHeight - viewportHeight;
32459
+ // Calculate desired scroll position to put the target into
32460
+ // the top left corner of the viewport, or as close as possible
32461
+ let targetScrollLeft = Math.min(elRect.left - boardRect.left, maxScrollLeft);
32462
+ let targetScrollTop = Math.min(elRect.top - boardRect.top, maxScrollTop);
32463
+ // Now animate both translate (for pan) and scale (for zoom) concurrently
32464
+ board.style.transition = 'transform 0.3s ease-in-out';
32465
+ // 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)
32471
+ 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 });
32447
32477
  }
32448
32478
  }
32449
32479
  if (!game || ((_a = model.state) === null || _a === void 0 ? void 0 : _a.uiFullscreen) || game.moveInProgress) {
@@ -32730,14 +32760,14 @@ class View {
32730
32760
  // displayed. We therefore allow them to cover the last_chall
32731
32761
  // dialog. On mobile, both dialogs are displayed simultaneously.
32732
32762
  if (game.last_chall) {
32733
- r.push(m(".chall-info", { style: { visibility: "visible" } }, [
32763
+ r.push(m(".chall-info", [
32734
32764
  glyph("info-sign"), nbsp(),
32735
32765
  // "Your opponent emptied the rack - you can challenge or pass"
32736
32766
  mt("span.pass-explain", "opponent_emptied_rack")
32737
32767
  ]));
32738
32768
  }
32739
32769
  if (game.showingDialog == "resign") {
32740
- r.push(m(".resign", { style: { visibility: "visible" } }, [
32770
+ r.push(m(".resign", [
32741
32771
  glyph("exclamation-sign"), nbsp(), ts("Viltu gefa leikinn?"), nbsp(),
32742
32772
  m("span.mobile-break", m("br")),
32743
32773
  m("span.yesnobutton", { onclick: () => game.confirmResign(true) }, [glyph("ok"), ts(" Já")]),
@@ -32747,7 +32777,7 @@ class View {
32747
32777
  }
32748
32778
  if (game.showingDialog == "pass") {
32749
32779
  if (game.last_chall) {
32750
- r.push(m(".pass-last", { style: { visibility: "visible" } }, [
32780
+ r.push(m(".pass-last", [
32751
32781
  glyph("forward"), nbsp(), ts("Segja pass?"),
32752
32782
  mt("span.pass-explain", "Viðureign lýkur þar með"),
32753
32783
  nbsp(),
@@ -32758,7 +32788,7 @@ class View {
32758
32788
  ]));
32759
32789
  }
32760
32790
  else {
32761
- r.push(m(".pass", { style: { visibility: "visible" } }, [
32791
+ r.push(m(".pass", [
32762
32792
  glyph("forward"), nbsp(), ts("Segja pass?"),
32763
32793
  mt("span.pass-explain", "2x3 pöss í röð ljúka viðureign"),
32764
32794
  nbsp(), m("span.mobile-break", m("br")),
@@ -32769,7 +32799,7 @@ class View {
32769
32799
  }
32770
32800
  }
32771
32801
  if (game.showingDialog == "exchange") {
32772
- r.push(m(".exchange", { style: { visibility: "visible" } }, [
32802
+ r.push(m(".exchange", [
32773
32803
  glyph("refresh"), nbsp(),
32774
32804
  ts("Smelltu á flísarnar sem þú vilt skipta"), nbsp(),
32775
32805
  m("span.mobile-break", m("br")),
@@ -32779,7 +32809,7 @@ class View {
32779
32809
  ]));
32780
32810
  }
32781
32811
  if (game.showingDialog == "chall") {
32782
- r.push(m(".chall", { style: { visibility: "visible" } }, [
32812
+ r.push(m(".chall", [
32783
32813
  glyph("ban-circle"), nbsp(), ts("Véfengja lögn?"),
32784
32814
  mt("span.pass-explain", "Röng véfenging kostar 10 stig"), nbsp(),
32785
32815
  m("span.mobile-break", m("br")),
@@ -33118,6 +33148,7 @@ class BaseGame {
33118
33148
  this.currentError = null;
33119
33149
  this.currentMessage = null;
33120
33150
  this.localturn = true;
33151
+ this.moveInProgress = false;
33121
33152
  // UI state
33122
33153
  this.showingDialog = null;
33123
33154
  this.selectedSq = null;
@@ -36210,12 +36241,12 @@ class Actions {
36210
36241
  score: json[userId].score || 0,
36211
36242
  timestamp: json[userId].timestamp || ''
36212
36243
  }));
36213
- // Sort by score descending, then by timestamp descending (newer first)
36244
+ // Sort by score descending, then by timestamp ascending (earlier first)
36214
36245
  entries.sort((a, b) => {
36215
36246
  if (b.score !== a.score) {
36216
36247
  return b.score - a.score;
36217
36248
  }
36218
- return b.timestamp.localeCompare(a.timestamp);
36249
+ return a.timestamp.localeCompare(b.timestamp);
36219
36250
  });
36220
36251
  this.model.leaderboard = entries;
36221
36252
  }
@@ -36581,13 +36612,16 @@ const SunCorona = {
36581
36612
  const MobileStatus = () => {
36582
36613
  return {
36583
36614
  view: (vnode) => {
36584
- const { riddle, selectedMoves, bestMove, onMoveClick } = vnode.attrs;
36615
+ const { riddle, selectedMoves, bestMove, onMoveClick, onStatsClick } = vnode.attrs;
36585
36616
  const { bestPossibleScore, globalBestScore } = riddle;
36586
36617
  // Determine if player achieved best possible score
36587
36618
  const achieved = bestMove !== undefined;
36588
36619
  const celebrate = bestMove && bestMove.word !== "";
36589
36620
  // Get player's current best score
36590
- const playerBestScore = selectedMoves.length > 0 ? selectedMoves[0].score : 0;
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);
36591
36625
  // Determine current leader score (may be this player or another)
36592
36626
  let leaderScore = 0;
36593
36627
  let isPlayerLeading = false;
@@ -36602,19 +36636,27 @@ const MobileStatus = () => {
36602
36636
  }
36603
36637
  return m(".mobile-status-container", [
36604
36638
  // Current word score (leftmost) - uses RiddleScore component in mobile mode
36605
- m(".mobile-status-item", m(RiddleScore, { riddle, mode: "mobile" })),
36606
- // Player's best score
36607
- m(".mobile-status-item.player-best", [
36608
- m(".mobile-status-label", ts("Þín besta:")),
36609
- m(".mobile-status-score", playerBestScore.toString())
36610
- ]),
36611
- // Current leader score
36612
- m(".mobile-status-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
36613
- m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
36614
- m(".mobile-status-score", leaderScore.toString())
36639
+ m(".mobile-status-item.left", m(RiddleScore, { riddle, mode: "mobile" })),
36640
+ // Interactive card containing player best and leader scores
36641
+ m(".mobile-status-card", {
36642
+ onclick: onStatsClick,
36643
+ title: ts("Tölfræði og stigatafla")
36644
+ }, [
36645
+ // Player's best score
36646
+ m(".mobile-status-card-item.player-best", [
36647
+ m(".mobile-status-label", ts("Þín besta:")),
36648
+ m(".mobile-status-score", playerBestScore.toString())
36649
+ ]),
36650
+ // Current leader score
36651
+ m(".mobile-status-card-item.leader" + (isPlayerLeading ? ".is-player" : ""), [
36652
+ m(".mobile-status-label", isPlayerLeading ? ts("Þú leiðir!") : ts("Leiðandi:")),
36653
+ m(".mobile-status-score", leaderScore.toString())
36654
+ ]),
36655
+ // Chevron indicator (overlaid at bottom center)
36656
+ m(".mobile-status-card-icon", glyph("chevron-down"))
36615
36657
  ]),
36616
36658
  // Best possible score
36617
- m(".mobile-status-item.best-possible"
36659
+ m(".mobile-status-item.right.best-possible"
36618
36660
  + (celebrate ? ".celebrate" : "")
36619
36661
  + (achieved ? ".achieved" : ""), {
36620
36662
  onclick: () => celebrate && onMoveClick(bestMove.word, bestMove.coord)
@@ -37087,7 +37129,7 @@ const RightSideTabs = () => {
37087
37129
  const GataDagsinsRightSide = {
37088
37130
  // Component containing both mobile status bar and desktop tabbed view
37089
37131
  view: (vnode) => {
37090
- const { view, selectedMoves, bestMove } = vnode.attrs;
37132
+ const { view, selectedMoves, bestMove, onStatsClick } = vnode.attrs;
37091
37133
  const { riddle } = view.model;
37092
37134
  const handleMoveClick = (word, coord) => {
37093
37135
  if (riddle && word && coord) {
@@ -37101,7 +37143,8 @@ const GataDagsinsRightSide = {
37101
37143
  riddle,
37102
37144
  selectedMoves,
37103
37145
  bestMove,
37104
- onMoveClick: handleMoveClick
37146
+ onMoveClick: handleMoveClick,
37147
+ onStatsClick
37105
37148
  })),
37106
37149
  // Desktop-only tabbed view (hidden on mobile, visible on desktop)
37107
37150
  m(".gatadagsins-thermometer-column", m(RightSideTabs, {
@@ -37131,64 +37174,72 @@ const GataDagsinsRightSide = {
37131
37174
  const GataDagsinsHelp = {
37132
37175
  view: (vnode) => {
37133
37176
  const closeHelp = vnode.attrs.onClose;
37134
- return m(".modal-dialog.gatadagsins-help", m(".modal-content", [
37135
- // Header with close button
37136
- m(".modal-header", [
37137
- m("h2", "Um Gátu dagsins"),
37138
- m("button.close", {
37139
- onclick: closeHelp,
37140
- "aria-label": "Loka"
37141
- }, m("span", { "aria-hidden": "true" }, "×"))
37142
- ]),
37143
- // Body with help content
37144
- m(".modal-body", [
37145
- m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir að finna " +
37146
- "stigahæsta orðið sem hægt er að mynda með gefnum stöfum."),
37147
- m("h3", "Hvernig á að spila"),
37148
- m("ul", [
37149
- m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
37150
- m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
37151
- m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
37152
- m("li", "Orðin verða að tengjast við stafi sem fyrir eru á borðinu."),
37153
- m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
37154
- m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
37155
- ]),
37156
- m("h3", "Stigagjöf"),
37157
- m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
37158
- m("ul", [
37159
- m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
37160
- m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
37161
- m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
37162
- m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
37163
- ]),
37164
- m("h3", "Hitamælir"),
37165
- m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
37166
- m("ul", [
37167
- m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
37168
- m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
37169
- m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
37170
- m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
37177
+ return [
37178
+ // Backdrop
37179
+ m(".modal-backdrop-netskrafl", {
37180
+ onclick: (e) => { e.preventDefault(); },
37181
+ onwheel: (e) => { e.preventDefault(); e.stopPropagation(); },
37182
+ ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
37183
+ }),
37184
+ m(".modal-dialog.gatadagsins-help", m(".modal-content", [
37185
+ // Header with close button
37186
+ m(".modal-header", [
37187
+ m("h2", "Um Gátu dagsins"),
37188
+ m("button.close", {
37189
+ onclick: closeHelp,
37190
+ "aria-label": "Loka"
37191
+ }, m("span", { "aria-hidden": "true" }, "×"))
37171
37192
  ]),
37172
- m("h3", "Ábendingar"),
37173
- m("ul", [
37174
- m("li", "Reyndu nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
37175
- m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
37176
- m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
37177
- m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
37178
- ]),
37179
- m("h3", "Um leikinn"),
37180
- m("p", [
37181
- "Gáta dagsins er systkini ",
37182
- m("a", { href: "https://netskrafl.is", target: "_blank" }, "Netskrafls"),
37183
- ", hins sívinsæla íslenska krossgátuleiks á netinu. ",
37184
- "Leikurinn er þróaður af Miðeind ehf."
37193
+ // Body with help content
37194
+ m(".modal-body", [
37195
+ m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir finna " +
37196
+ "stigahæsta orðið sem hægt er mynda með gefnum stöfum."),
37197
+ m("h3", "Hvernig á spila"),
37198
+ m("ul", [
37199
+ m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
37200
+ m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
37201
+ m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
37202
+ m("li", "Orðin verða tengjast við stafi sem fyrir eru á borðinu."),
37203
+ m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
37204
+ m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
37205
+ ]),
37206
+ m("h3", "Stigagjöf"),
37207
+ m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
37208
+ m("ul", [
37209
+ m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
37210
+ m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
37211
+ m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
37212
+ m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
37213
+ ]),
37214
+ m("h3", "Hitamælir"),
37215
+ m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
37216
+ m("ul", [
37217
+ m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
37218
+ m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
37219
+ m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
37220
+ m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
37221
+ ]),
37222
+ m("h3", "Ábendingar"),
37223
+ m("ul", [
37224
+ m("li", "Reyndu að nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
37225
+ m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
37226
+ m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
37227
+ m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
37228
+ ]),
37229
+ m("h3", "Um leikinn"),
37230
+ m("p", [
37231
+ "Gáta dagsins er systkini ",
37232
+ m("a", { href: "https://netskrafl.is", target: "_blank" }, "Netskrafls"),
37233
+ ", hins sívinsæla íslenska krossgátuleiks á netinu. ",
37234
+ "Leikurinn er þróaður af Miðeind ehf."
37235
+ ]),
37185
37236
  ]),
37186
- ]),
37187
- // Footer with close button
37188
- m(".modal-footer", m("button.btn.btn-primary", {
37189
- onclick: closeHelp
37190
- }, "Loka"))
37191
- ]));
37237
+ // Footer with close button
37238
+ m(".modal-footer", m("button.btn.btn-primary", {
37239
+ onclick: closeHelp
37240
+ }, "Loka"))
37241
+ ])),
37242
+ ];
37192
37243
  }
37193
37244
  };
37194
37245
 
@@ -37225,19 +37276,22 @@ const StatsModal = () => {
37225
37276
  };
37226
37277
  return [
37227
37278
  // Backdrop
37228
- m(".modal-backdrop", {
37229
- onclick: onClose
37279
+ m(".modal-backdrop-netskrafl", {
37280
+ onclick: (e) => { e.preventDefault(); },
37281
+ onwheel: (e) => { e.preventDefault(); e.stopPropagation(); },
37282
+ ontouchmove: (e) => { e.preventDefault(); e.stopPropagation(); }
37230
37283
  }),
37231
37284
  // Modal dialog
37232
- m(".modal-dialog.stats-modal", [
37285
+ m(".modal-dialog.stats-modal", {
37286
+ onwheel: (e) => { e.stopPropagation(); },
37287
+ ontouchmove: (e) => { e.stopPropagation(); }
37288
+ }, [
37233
37289
  m(".modal-content", [
37234
- // Header with close button
37235
- m(".modal-header", [
37236
- m("h2", ts("Tölfræði")),
37237
- m("button.close", {
37238
- onclick: onClose
37239
- }, "×")
37240
- ]),
37290
+ // Close button in top right
37291
+ m("button.close", {
37292
+ onclick: onClose,
37293
+ "aria-label": "Loka"
37294
+ }, glyph("remove")),
37241
37295
  // Tab navigation
37242
37296
  m(TabBar, {
37243
37297
  tabs,
@@ -37264,30 +37318,6 @@ const StatsModal = () => {
37264
37318
  };
37265
37319
  };
37266
37320
 
37267
- /*
37268
-
37269
- MobileStatsButton.ts
37270
-
37271
- Button to open stats modal on mobile
37272
-
37273
- Copyright (C) 2025 Miðeind ehf.
37274
- Author: Vilhjálmur Þorsteinsson
37275
-
37276
- The Creative Commons Attribution-NonCommercial 4.0
37277
- International Public License (CC-BY-NC 4.0) applies to this software.
37278
- For further information, see https://github.com/mideind/Netskrafl
37279
-
37280
- */
37281
- const MobileStatsButton = {
37282
- view: (vnode) => {
37283
- const { onClick } = vnode.attrs;
37284
- return m(".mobile-stats-button", {
37285
- onclick: onClick,
37286
- title: "Tölfræði og stigatafla"
37287
- }, m(".stats-icon", glyph("stats")));
37288
- }
37289
- };
37290
-
37291
37321
  /*
37292
37322
 
37293
37323
  GataDagsins.ts
@@ -37393,7 +37423,7 @@ const GataDagsins$1 = () => {
37393
37423
  // Board and rack component (left side)
37394
37424
  m(GataDagsinsBoardAndRack, { view }),
37395
37425
  // Right-side component with scores and comparisons
37396
- m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
37426
+ m(GataDagsinsRightSide, { view, selectedMoves, bestMove, onStatsClick: toggleStatsModal }),
37397
37427
  // Blank dialog
37398
37428
  riddle.askingForBlank
37399
37429
  ? m(BlankDialog, { game: riddle })
@@ -37407,13 +37437,8 @@ const GataDagsins$1 = () => {
37407
37437
  ((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
37408
37438
  // Custom Info button for GataDagsins that shows help dialog
37409
37439
  m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
37410
- // Mobile stats button (hidden on desktop)
37411
- m(MobileStatsButton, { onClick: toggleStatsModal }),
37412
37440
  // Help dialog and backdrop
37413
- showHelp ? [
37414
- m(".modal-backdrop", { onclick: (e) => { e.preventDefault(); } }),
37415
- m(GataDagsinsHelp, { onClose: toggleHelp })
37416
- ] : "",
37441
+ showHelp ? m(GataDagsinsHelp, { onClose: toggleHelp }) : "",
37417
37442
  // Stats modal and backdrop (mobile only)
37418
37443
  showStatsModal ? m(StatsModal, { view, onClose: toggleStatsModal }) : "",
37419
37444
  ]);