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