packing-bingo 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -126,7 +126,7 @@ function bingoLines(size, free = false) {
126
126
  // src/messages.ts
127
127
  var ja = {
128
128
  title: "\u6301\u3061\u3082\u306E\u30D3\u30F3\u30B4",
129
- tag: "\u4FEE\u5B66\u65C5\u884C",
129
+ tag: "",
130
130
  subtitle: "",
131
131
  packed: "\u305D\u308D\u3063\u305F",
132
132
  bingo: "BINGO",
@@ -134,6 +134,7 @@ var ja = {
134
134
  combo: (n) => `${n} COMBO`,
135
135
  comboPop: (n) => `${n} COMBO!`,
136
136
  reset: "\u30EA\u30BB\u30C3\u30C8",
137
+ shuffle: "\u{1F500} \u30B7\u30E3\u30C3\u30D5\u30EB",
137
138
  soundOn: "\u{1F50A} \u97F3\u3042\u308A",
138
139
  soundOff: "\u{1F507} \u97F3\u306A\u3057",
139
140
  stamp: "OK",
@@ -147,7 +148,7 @@ var ja = {
147
148
  bannerMulti: (n) => `\u4E00\u6C17\u306B ${n} \u30E9\u30A4\u30F3\uFF01`,
148
149
  statusStart: () => "",
149
150
  statusLines: (lines, left) => `\u30D3\u30F3\u30B4 ${lines} \u30E9\u30A4\u30F3\uFF01 \u306E\u3053\u308A ${left} \u3053`,
150
- statusReach: (labels) => `\u30EA\u30FC\u30C1\uFF01 \u3042\u3068${labels.slice(0, 2).map((l) => `\u300C${l}\u300D`).join("\u304B")}${labels.length > 2 ? "\u306A\u3069" : ""}\u3067\u30D3\u30F3\u30B4`,
151
+ statusReach: () => "",
151
152
  statusDone: "\u{1F389} \u30D1\u30FC\u30D5\u30A7\u30AF\u30C8\uFF01\u5FD8\u308C\u7269\u30BC\u30ED\u3067\u51FA\u767A\u3067\u304D\u307E\u3059",
152
153
  perfectTag: "\u{1F3C6} PERFECT",
153
154
  perfectStamp: "\u6E96\u5099\n\u5B8C\u4E86",
@@ -166,7 +167,7 @@ var ja = {
166
167
  };
167
168
  var en = {
168
169
  title: "Packing Bingo",
169
- tag: "School trip",
170
+ tag: "",
170
171
  subtitle: "",
171
172
  packed: "PACKED",
172
173
  bingo: "BINGO",
@@ -174,6 +175,7 @@ var en = {
174
175
  combo: (n) => `${n} COMBO`,
175
176
  comboPop: (n) => `${n} COMBO!`,
176
177
  reset: "Reset",
178
+ shuffle: "\u{1F500} Shuffle",
177
179
  soundOn: "\u{1F50A} Sound on",
178
180
  soundOff: "\u{1F507} Muted",
179
181
  stamp: "OK",
@@ -187,7 +189,7 @@ var en = {
187
189
  bannerMulti: (n) => `${n} lines at once!`,
188
190
  statusStart: () => "",
189
191
  statusLines: (lines, left) => `${lines} bingo line${lines === 1 ? "" : "s"}! ${left} to go`,
190
- statusReach: (labels) => `Reach! Pack ${labels.slice(0, 2).join(" or ")}${labels.length > 2 ? " (or more)" : ""} for a bingo`,
192
+ statusReach: () => "",
191
193
  statusDone: "\u{1F389} Perfect! Nothing left behind.",
192
194
  perfectTag: "\u{1F3C6} PERFECT",
193
195
  perfectStamp: "ALL\nSET",
@@ -293,6 +295,11 @@ function createSound(isMuted) {
293
295
  },
294
296
  tick: () => tone(1760, 0, 0.03, "square", 0.03),
295
297
  note: (k, at = 0) => tone(step(k), at, 0.12, "square", 0.05),
298
+ shuffle() {
299
+ for (let i = 0; i < 7; i++) noise(i * 0.042, 0.05, 0.16, 1600 + i * 260);
300
+ tone(hz(7), 0.34, 0.12, "triangle", 0.14);
301
+ tone(hz(12), 0.42, 0.2, "triangle", 0.14);
302
+ },
296
303
  close() {
297
304
  if (ac) void ac.close().catch(() => {
298
305
  });
@@ -320,7 +327,6 @@ function itemsKey(items) {
320
327
  }
321
328
 
322
329
  // src/styles.ts
323
- var FONT_URL = "https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap";
324
330
  var CSS = `
325
331
  .pkb{--pk-sun:var(--pkb-sun,#FFD54F);--pk-ink:var(--pkb-ink,#2A1F14);--pk-paper:var(--pkb-paper,#FFFDF6);--pk-red:var(--pkb-red,#E8473B);--pk-mint:var(--pkb-mint,#BDEBD2);--pk-display:"Dela Gothic One",var(--pkb-font-fallback,sans-serif);--pk-body:"M PLUS Rounded 1c","Hiragino Maru Gothic ProN",var(--pkb-font-fallback,sans-serif);--pk-n:4;--pk-s:1;--pk-d:1;container-type:inline-size;isolation:isolate;width:100%}
326
332
  .pkb,.pkb *{box-sizing:border-box;margin:0;padding:0}
@@ -332,6 +338,7 @@ var CSS = `
332
338
  .pkb-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px 16px;margin-bottom:18px}
333
339
  .pkb-tags{display:flex;gap:8px;align-items:center}
334
340
  .pkb-kicker{display:inline-block;background:var(--pk-ink);color:var(--pk-sun);font-weight:800;font-size:11px;letter-spacing:.14em;padding:4px 10px;border-radius:999px}
341
+ .pkb-kicker:empty{display:none}
335
342
  .pkb-kicker.pkb-gold{color:#fff;background:linear-gradient(90deg,#E8473B,#FF9F1C,#1FA971,#2F6FED,#E8473B);background-size:200% 100%;animation:pkb-rb 2s linear infinite}
336
343
  @keyframes pkb-rb{to{background-position:-200% 0}}
337
344
  .pkb button{font-family:var(--pk-body)}
@@ -388,8 +395,13 @@ var CSS = `
388
395
  .pkb-lines .pkb-o{stroke:var(--pk-ink);stroke-width:calc(6px*var(--pk-d))}
389
396
  .pkb-lines .pkb-draw line{animation:pkb-draw .45s ease-out .3s both}
390
397
  @keyframes pkb-draw{from{stroke-dashoffset:1}}
391
- .pkb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
392
- .pkb-reset{appearance:none;flex:none;border:2px solid var(--pk-ink);background:transparent;border-radius:999px;padding:5px 12px;font-weight:800;font-size:12px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
398
+ .pkb-foot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 10px;margin-top:18px;font-weight:800;font-size:13px;word-break:auto-phrase}
399
+ .pkb-foot>span{flex:1 1 14em}
400
+ .pkb-foot>span:empty{flex-basis:0}
401
+ .pkb-acts{display:flex;gap:8px;flex:none;margin-left:auto}
402
+ .pkb-reset,.pkb-shuf{appearance:none;flex:none;border:2px solid var(--pk-ink);background:transparent;border-radius:999px;padding:5px 12px;font-weight:800;font-size:12px;line-height:1.4;color:var(--pk-ink);cursor:pointer}
403
+ .pkb-shuf{background:var(--pk-paper);box-shadow:2px 2px 0 var(--pk-ink)}
404
+ .pkb-shuf:active{transform:translate(2px,2px);box-shadow:none}
393
405
  .pkb-flash{position:absolute;inset:0;z-index:7;background:#fff;opacity:0;pointer-events:none}
394
406
  .pkb-flash.pkb-go{animation:pkb-flash .4s ease-out}
395
407
  @keyframes pkb-flash{0%{opacity:.8}100%{opacity:0}}
@@ -437,6 +449,7 @@ var CONFETTI = ["#E8473B", "#2F6FED", "#1FA971", "#FFFDF6", "#2A1F14", "#FF9F1C"
437
449
  var COMBO_COLORS = ["#FF9F1C", "#1FA971", "#2F6FED", "#9B5DE5", "#E8473B"];
438
450
  var COMBO_MS = 2500;
439
451
  var MUTE_KEY = "packing-bingo-muted";
452
+ var ORDER_PREFIX = "packing-bingo-order:";
440
453
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
441
454
  function readStore(key) {
442
455
  try {
@@ -451,29 +464,25 @@ function writeStore(key, value) {
451
464
  } catch {
452
465
  }
453
466
  }
454
- function ensureAssets(fonts) {
455
- if (!document.getElementById("pkb-styles")) {
456
- const s = document.createElement("style");
457
- s.id = "pkb-styles";
458
- s.textContent = CSS;
459
- document.head.appendChild(s);
460
- }
461
- if (fonts && !document.getElementById("pkb-fonts")) {
462
- const l = document.createElement("link");
463
- l.id = "pkb-fonts";
464
- l.rel = "stylesheet";
465
- l.href = FONT_URL;
466
- document.head.appendChild(l);
467
- }
467
+ var glintDelay = (c, n) => `${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
468
+ var lineKey = (l) => l.slice().sort((a, b) => a - b).join(",");
469
+ var FLY_MS = 560;
470
+ function ensureStyles() {
471
+ if (document.getElementById("pkb-styles")) return;
472
+ const s = document.createElement("style");
473
+ s.id = "pkb-styles";
474
+ s.textContent = CSS;
475
+ document.head.appendChild(s);
468
476
  }
469
- function template(L, M, demo) {
477
+ function template(L, M, { demo, shuffle, vibrate }) {
470
478
  const n = L.size;
479
+ const input = vibrate ? `<input type="checkbox" switch role="checkbox">` : `<input type="checkbox">`;
471
480
  const tiles = Array.from({ length: n * n }, (_, c) => {
472
- const gd = `--pkb-gd:${+((Math.floor(c / n) + c % n) * 0.36 / Math.max(1, n - 1)).toFixed(3)}s`;
481
+ const gd = `--pkb-gd:${glintDelay(c, n)}`;
473
482
  if (c === L.center)
474
483
  return `<div class="pkb-tile pkb-free pkb-on" style="${gd}"><span class="pkb-stamp">${esc(M.free)}</span></div>`;
475
484
  const it = L.items[L.itemCell.indexOf(c)];
476
- return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}"><input type="checkbox"><span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`;
485
+ return `<label class="pkb-tile" data-reach="${esc(M.reachBadge)}" style="${gd}">${input}<span class="pkb-emo" aria-hidden="true">${esc(it.emoji)}</span><span class="pkb-lbl">${esc(it.label).replace(/・/g, "\u30FB<wbr>")}</span><span class="pkb-stamp" aria-hidden="true">${esc(M.stamp)}</span></label>`;
477
486
  }).join("");
478
487
  const scale = `--pk-n:${n};--pk-s:${+Math.min(1.35, 4 / n).toFixed(3)};--pk-d:${+Math.min(1, 4 / n).toFixed(3)}`;
479
488
  const demoBar = demo ? `<div class="pkb-demo"><span>${esc(M.demoLabel)}</span><button type="button" data-r="dB">${esc(M.demoBingo)}</button><button type="button" data-r="dP">${esc(M.demoPerfect)}</button><button type="button" data-r="dR">${esc(M.demoRestore)}</button></div>` : "";
@@ -488,7 +497,7 @@ function template(L, M, demo) {
488
497
  <div class="pkb-combo" data-r="combo"><b data-r="cmbN"></b><span class="pkb-bar"><i data-r="cbar"></i></span></div></div></div>
489
498
  <div class="pkb-gw"><div class="pkb-grid">${tiles}</div><svg class="pkb-lines" aria-hidden="true"></svg>
490
499
  <div class="pkb-banner" data-r="banner" aria-hidden="true"><b data-r="bT"></b><span data-r="bS"></span></div></div>
491
- <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div>
500
+ <div class="pkb-foot"><span data-r="msg" aria-live="polite"></span><div class="pkb-acts">${shuffle ? `<button type="button" class="pkb-shuf" data-r="shuf">${esc(M.shuffle)}</button>` : ""}<button type="button" class="pkb-reset" data-r="reset">${esc(M.reset)}</button></div></div>
492
501
  <div class="pkb-flash" data-r="flash"></div>
493
502
  <div class="pkb-perfect" data-r="perfect"><div class="pkb-card" role="dialog" aria-label="${esc(M.perfectTitle)}">
494
503
  <div class="pkb-medal">${esc(M.perfectStamp)}</div><h3 class="pkb-ptitle">${esc(M.perfectTitle)}</h3><p class="pkb-pbody" data-r="pbody">${esc(perfectText(M, L.items.length))}</p>
@@ -499,12 +508,22 @@ function template(L, M, demo) {
499
508
  var perfectText = (M, total) => typeof M.perfectBody === "function" ? M.perfectBody(total) : M.perfectBody;
500
509
  function createPackingBingo(container, options = {}) {
501
510
  const L = resolveLayout(options);
502
- const { items, lines, cellLines } = L;
511
+ const { items, cellLines } = L;
503
512
  const n = L.size, N = items.length;
504
513
  const normalize = (v) => Array.from({ length: N }, (_, i) => !!v?.[i]);
514
+ const identity = () => Array.from({ length: N }, (_, s) => s);
515
+ const isIdentity = (o) => o.every((v, s) => v === s);
516
+ const validOrder = (o) => Array.isArray(o) && o.length === N && o.every((v) => Number.isInteger(v) && v >= 0 && v < N) && new Set(o).size === N;
517
+ let order = identity();
518
+ const slotOf = identity();
519
+ let lines = L.lines;
505
520
  const demoCells = [...cellLines[0], ...cellLines[2 * n].slice(1, -1)];
506
- const demoItems = new Set(L.itemCell.flatMap((c, i) => demoCells.includes(c) ? [i] : []));
507
- const demoStart = () => Array.from({ length: N }, (_, i) => demoItems.has(i));
521
+ const demoSlots = L.itemCell.flatMap((c, s) => demoCells.includes(c) ? [s] : []);
522
+ const demoStart = () => {
523
+ const on = normalize(void 0);
524
+ demoSlots.forEach((s) => on[order[s]] = true);
525
+ return on;
526
+ };
508
527
  const far = (c) => Math.floor(c / n) + c % n;
509
528
  const dScale = Math.min(1, 4 / n);
510
529
  const M = { ...ja, ...options.messages };
@@ -513,7 +532,9 @@ function createPackingBingo(container, options = {}) {
513
532
  if (options.subtitle != null) M.subtitle = options.subtitle;
514
533
  const controlled = !!options.controlled;
515
534
  const storageKey = resolveStorageKey(options);
535
+ const orderKey = storageKey && ORDER_PREFIX + storageKey;
516
536
  const demo = !!options.demo;
537
+ const vibrate = options.vibrate !== false;
517
538
  const calm = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
518
539
  let dead = false;
519
540
  const timers = /* @__PURE__ */ new Set();
@@ -526,8 +547,8 @@ function createPackingBingo(container, options = {}) {
526
547
  };
527
548
  const wait = (ms) => new Promise((r) => window.setTimeout(r, ms));
528
549
  let gen = 0;
529
- ensureAssets(options.fonts !== false);
530
- container.innerHTML = template(L, M, demo);
550
+ ensureStyles();
551
+ container.innerHTML = template(L, M, { demo, shuffle: options.shuffle !== false, vibrate });
531
552
  const $ = (r) => {
532
553
  const el = container.querySelector(`[data-r="${r}"]`);
533
554
  if (!el) throw new Error(`[packing-bingo] internal: missing element "${r}"`);
@@ -704,6 +725,7 @@ function createPackingBingo(container, options = {}) {
704
725
  r.addEventListener("animationend", () => r.remove());
705
726
  };
706
727
  const buzz = (pattern) => {
728
+ if (!vibrate) return;
707
729
  const ua = navigator.userActivation;
708
730
  if (ua && !ua.hasBeenActive) return;
709
731
  try {
@@ -762,7 +784,12 @@ function createPackingBingo(container, options = {}) {
762
784
  const ctr = (t) => {
763
785
  return { x: t.offsetLeft + t.offsetWidth / 2, y: t.offsetTop + t.offsetHeight / 2 };
764
786
  };
787
+ let landing = 0;
765
788
  function drawLines(fresh = []) {
789
+ if (landing) {
790
+ svg.innerHTML = "";
791
+ return;
792
+ }
766
793
  svg.setAttribute("viewBox", `0 0 ${gw.clientWidth} ${gw.clientHeight}`);
767
794
  svg.innerHTML = cur.map((k) => {
768
795
  const a = ctr(cellTiles[cellLines[k][0]]), b = ctr(cellTiles[cellLines[k][n - 1]]);
@@ -777,6 +804,7 @@ function createPackingBingo(container, options = {}) {
777
804
  combo = 0;
778
805
  $("combo").classList.remove("pkb-on");
779
806
  sfx.off();
807
+ buzz(8);
780
808
  return;
781
809
  }
782
810
  const now = performance.now();
@@ -799,7 +827,7 @@ function createPackingBingo(container, options = {}) {
799
827
  $("cmbN").textContent = M.combo(combo);
800
828
  replay($("combo"), "pkb-on");
801
829
  }
802
- buzz(12);
830
+ buzz(18);
803
831
  }
804
832
  $("cbar").addEventListener("animationend", () => {
805
833
  $("combo").classList.remove("pkb-on");
@@ -893,10 +921,11 @@ function createPackingBingo(container, options = {}) {
893
921
  $("combo").classList.remove("pkb-on");
894
922
  perfectEl.classList.remove("pkb-show");
895
923
  }
924
+ const completeLines = () => lines.flatMap((l, k) => l.every((i) => checked[i]) ? [k] : []);
896
925
  function render(src, quiet) {
897
926
  const events = [];
898
927
  const on = checked, c = on.filter(Boolean).length;
899
- cur = lines.flatMap((l, k) => l.every((i) => on[i]) ? [k] : []);
928
+ cur = completeLines();
900
929
  const fresh = cur.filter((k) => !prevLines.has(k));
901
930
  if ([...prevLines].some((k) => !cur.includes(k))) {
902
931
  cancelFx();
@@ -910,7 +939,7 @@ function createPackingBingo(container, options = {}) {
910
939
  }
911
940
  tiles.forEach((t, i) => {
912
941
  t.classList.toggle("pkb-on", on[i]);
913
- t.classList.toggle("pkb-line", hit.has(L.itemCell[i]));
942
+ t.classList.toggle("pkb-line", hit.has(L.itemCell[slotOf[i]]));
914
943
  t.classList.toggle("pkb-reach", reach.has(i));
915
944
  });
916
945
  freeTile?.classList.toggle("pkb-line", hit.has(L.center));
@@ -928,8 +957,8 @@ function createPackingBingo(container, options = {}) {
928
957
  lastN = cur.length;
929
958
  drawLines(fresh);
930
959
  if (src !== null) tapFx(src, on[src]);
931
- const newL = fresh.filter((k) => !gotL.has(k));
932
- newL.forEach((k) => gotL.add(k));
960
+ const newL = fresh.map((k) => lineKey(lines[k])).filter((key) => !gotL.has(key));
961
+ newL.forEach((key) => gotL.add(key));
933
962
  const linePts = 100 * newL.length * newL.length;
934
963
  score += linePts;
935
964
  if (quiet) {
@@ -974,8 +1003,9 @@ function createPackingBingo(container, options = {}) {
974
1003
  got.clear();
975
1004
  gotL.clear();
976
1005
  checked.forEach((v, i) => v && got.add(i));
977
- lines.forEach((l, k) => l.every((i) => checked[i]) && gotL.add(k));
978
- prevLines = new Set(gotL);
1006
+ const done = completeLines();
1007
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1008
+ prevLines = new Set(done);
979
1009
  prevReach = /* @__PURE__ */ new Set();
980
1010
  prevAll = checked.every(Boolean);
981
1011
  gotP = prevAll;
@@ -998,6 +1028,77 @@ function createPackingBingo(container, options = {}) {
998
1028
  options.onChange?.(checked.slice());
999
1029
  flush(events);
1000
1030
  }
1031
+ function place(next) {
1032
+ order = next;
1033
+ order.forEach((i, s) => {
1034
+ slotOf[i] = s;
1035
+ cellTiles[L.itemCell[s]] = tiles[i];
1036
+ });
1037
+ lines = L.lines.map((l) => l.map((s) => order[s]));
1038
+ const focused = document.activeElement;
1039
+ cellTiles.forEach((t, c) => {
1040
+ t.style.setProperty("--pkb-gd", glintDelay(c, n));
1041
+ grid.appendChild(t);
1042
+ });
1043
+ if (focused instanceof HTMLElement && grid.contains(focused) && document.activeElement !== focused)
1044
+ focused.focus({ preventScroll: true });
1045
+ }
1046
+ const saveOrder = () => {
1047
+ if (!orderKey) return;
1048
+ writeStore(orderKey, JSON.stringify(order));
1049
+ };
1050
+ let flights = [];
1051
+ function arrange(next) {
1052
+ if (!checked.every(Boolean)) cancelFx();
1053
+ const from = calm ? null : tiles.map((t) => t.getBoundingClientRect());
1054
+ flights.forEach((a) => a.cancel());
1055
+ flights = [];
1056
+ place(next);
1057
+ lines.forEach((l) => {
1058
+ if (l.every((i) => got.has(i))) gotL.add(lineKey(l));
1059
+ });
1060
+ const done = completeLines();
1061
+ done.forEach((k) => gotL.add(lineKey(lines[k])));
1062
+ prevLines = new Set(done);
1063
+ render(null, true);
1064
+ saveOrder();
1065
+ if (!from || typeof Element.prototype.animate !== "function") return;
1066
+ tiles.forEach((t, i) => {
1067
+ const a = from[i], b = t.getBoundingClientRect();
1068
+ const dx = a.left - b.left, dy = a.top - b.top;
1069
+ if (Math.abs(dx) + Math.abs(dy) < 1) return;
1070
+ flights.push(
1071
+ t.animate([{ transform: `translate(${dx}px,${dy}px)` }, { transform: "none" }], {
1072
+ duration: FLY_MS - 140,
1073
+ delay: Math.random() * 140,
1074
+ easing: "cubic-bezier(.3,1.25,.5,1)",
1075
+ fill: "backwards"
1076
+ })
1077
+ );
1078
+ });
1079
+ clearTimeout(landing);
1080
+ landing = window.setTimeout(() => {
1081
+ landing = 0;
1082
+ flights = [];
1083
+ if (!dead) drawLines(cur);
1084
+ }, FLY_MS);
1085
+ svg.innerHTML = "";
1086
+ }
1087
+ function shuffleCard() {
1088
+ let next = order.slice();
1089
+ do {
1090
+ for (let s = N - 1; s > 0; s--) {
1091
+ const r = Math.floor(Math.random() * (s + 1));
1092
+ [next[s], next[r]] = [next[r], next[s]];
1093
+ }
1094
+ } while (next.every((v, s) => v === order[s]));
1095
+ arrange(next);
1096
+ sfx.shuffle();
1097
+ buzz([10, 40, 10, 40, 10, 40, 24]);
1098
+ }
1099
+ const unshuffle = () => {
1100
+ if (!isIdentity(order)) arrange(identity());
1101
+ };
1001
1102
  grid.addEventListener("change", (e) => {
1002
1103
  const i = ins.indexOf(e.target);
1003
1104
  if (i < 0) return;
@@ -1005,7 +1106,11 @@ function createPackingBingo(container, options = {}) {
1005
1106
  next[i] = !next[i];
1006
1107
  request(next, i);
1007
1108
  });
1008
- $("reset").addEventListener("click", () => request(normalize(void 0), null, { reset: true }));
1109
+ container.querySelector('[data-r="shuf"]')?.addEventListener("click", shuffleCard);
1110
+ $("reset").addEventListener("click", () => {
1111
+ unshuffle();
1112
+ request(normalize(void 0), null, { reset: true });
1113
+ });
1009
1114
  $("close").addEventListener("click", () => perfectEl.classList.remove("pkb-show"));
1010
1115
  perfectEl.addEventListener("click", (e) => {
1011
1116
  if (e.target === perfectEl) perfectEl.classList.remove("pkb-show");
@@ -1044,9 +1149,21 @@ function createPackingBingo(container, options = {}) {
1044
1149
  void run(miss.map((i, k) => k < miss.length - 1 ? { i, delay: 170, quiet: true } : { i, delay: 450 }));
1045
1150
  });
1046
1151
  $("dR").addEventListener("click", () => {
1047
- if (!busy) request(demoStart(), null, { reset: true });
1152
+ if (busy) return;
1153
+ unshuffle();
1154
+ request(demoStart(), null, { reset: true });
1048
1155
  });
1049
1156
  }
1157
+ let startOrder = null;
1158
+ if (orderKey) {
1159
+ try {
1160
+ const saved = JSON.parse(readStore(orderKey) ?? "null");
1161
+ if (validOrder(saved)) startOrder = saved;
1162
+ } catch {
1163
+ }
1164
+ }
1165
+ if (!startOrder && validOrder(options.defaultOrder)) startOrder = options.defaultOrder.slice();
1166
+ if (startOrder && !isIdentity(startOrder)) place(startOrder);
1050
1167
  let initial = null;
1051
1168
  if (storageKey) {
1052
1169
  try {
@@ -1078,19 +1195,35 @@ function createPackingBingo(container, options = {}) {
1078
1195
  flush(apply(n2, changed[0], false));
1079
1196
  },
1080
1197
  reset() {
1081
- if (!dead) request(normalize(void 0), null, { reset: true });
1198
+ if (dead) return;
1199
+ unshuffle();
1200
+ request(normalize(void 0), null, { reset: true });
1201
+ },
1202
+ shuffle() {
1203
+ if (!dead) shuffleCard();
1082
1204
  },
1083
1205
  refresh() {
1084
1206
  if (!dead) render(null, true);
1085
1207
  },
1086
1208
  getState() {
1087
- return { checked: checked.slice(), lines: cur.slice(), score, maxCombo, complete: checked.every(Boolean), size: n, free: L.free };
1209
+ return {
1210
+ checked: checked.slice(),
1211
+ lines: cur.slice(),
1212
+ score,
1213
+ maxCombo,
1214
+ complete: checked.every(Boolean),
1215
+ size: n,
1216
+ free: L.free,
1217
+ order: order.slice()
1218
+ };
1088
1219
  },
1089
1220
  destroy() {
1090
1221
  if (dead) return;
1091
1222
  dead = true;
1092
1223
  timers.forEach((id) => clearTimeout(id));
1093
1224
  timers.clear();
1225
+ clearTimeout(landing);
1226
+ flights.forEach((a) => a.cancel());
1094
1227
  cancelAnimationFrame(raf);
1095
1228
  cancelAnimationFrame(tw);
1096
1229
  ro?.disconnect();
@@ -1103,7 +1236,7 @@ function createPackingBingo(container, options = {}) {
1103
1236
  // src/react.tsx
1104
1237
  var import_jsx_runtime = require("react/jsx-runtime");
1105
1238
  var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, ref) {
1106
- const { checked, defaultChecked, className, style, onChange, onBingo, onPerfect, messages, ...config } = props;
1239
+ const { checked, defaultChecked, defaultOrder, className, style, onChange, onBingo, onPerfect, messages, fonts: _fonts, ...config } = props;
1107
1240
  const host = (0, import_react.useRef)(null);
1108
1241
  const inst = (0, import_react.useRef)(null);
1109
1242
  const callbacks = (0, import_react.useRef)({ onChange, onBingo, onPerfect });
@@ -1115,6 +1248,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1115
1248
  const storageKey = resolveStorageKey({ ...config, controlled });
1116
1249
  const latestKey = (0, import_react.useRef)(storageKey);
1117
1250
  if (controlled) latest.current = checked;
1251
+ const latestOrder = (0, import_react.useRef)(defaultOrder);
1118
1252
  const messageShape = messages ? Object.fromEntries(Object.entries(messages).map(([k, v]) => [k, typeof v === "function" ? "\u0192" : v])) : null;
1119
1253
  const key = JSON.stringify({ ...config, messageShape, controlled });
1120
1254
  (0, import_react.useEffect)(() => {
@@ -1130,6 +1264,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1130
1264
  messages: liveMessages,
1131
1265
  controlled,
1132
1266
  defaultChecked: controlled || latestKey.current === storageKey ? latest.current : defaultChecked,
1267
+ defaultOrder: latestKey.current === storageKey ? latestOrder.current : defaultOrder,
1133
1268
  onChange: (next) => {
1134
1269
  if (!controlled) latest.current = next;
1135
1270
  callbacks.current.onChange?.(next);
@@ -1139,7 +1274,9 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1139
1274
  });
1140
1275
  inst.current = instance;
1141
1276
  return () => {
1142
- if (!controlled) latest.current = instance.getState().checked;
1277
+ const state = instance.getState();
1278
+ if (!controlled) latest.current = state.checked;
1279
+ latestOrder.current = state.order;
1143
1280
  latestKey.current = storageKey;
1144
1281
  instance.destroy();
1145
1282
  if (inst.current === instance) inst.current = null;
@@ -1155,6 +1292,7 @@ var PackingBingo = (0, import_react.forwardRef)(function PackingBingo2(props, re
1155
1292
  ref,
1156
1293
  () => ({
1157
1294
  reset: () => inst.current?.reset(),
1295
+ shuffle: () => inst.current?.shuffle(),
1158
1296
  setChecked: (next) => inst.current?.setChecked(next, { animate: true }),
1159
1297
  getState: () => inst.current?.getState() ?? null
1160
1298
  }),
package/dist/index.d.cts CHANGED
@@ -12,8 +12,10 @@ interface PackingBingoProps extends Omit<PackingBingoOptions, "controlled" | "de
12
12
  style?: CSSProperties;
13
13
  }
14
14
  interface PackingBingoHandle {
15
- /** Clear the card and score (calls onChange). */
15
+ /** Clear the card and score (calls onChange), and put the items back in their own squares. */
16
16
  reset(): void;
17
+ /** Move the items to random squares, as the 🔀 button does. Checks stay with their items. */
18
+ shuffle(): void;
17
19
  /** Set the card. A one-item change plays like a tap; bigger jumps apply quietly. */
18
20
  setChecked(checked: readonly boolean[]): void;
19
21
  getState(): PackingBingoState | null;
package/dist/index.d.ts CHANGED
@@ -12,8 +12,10 @@ interface PackingBingoProps extends Omit<PackingBingoOptions, "controlled" | "de
12
12
  style?: CSSProperties;
13
13
  }
14
14
  interface PackingBingoHandle {
15
- /** Clear the card and score (calls onChange). */
15
+ /** Clear the card and score (calls onChange), and put the items back in their own squares. */
16
16
  reset(): void;
17
+ /** Move the items to random squares, as the 🔀 button does. Checks stay with their items. */
18
+ shuffle(): void;
17
19
  /** Set the card. A one-item change plays like a tap; bigger jumps apply quietly. */
18
20
  setChecked(checked: readonly boolean[]): void;
19
21
  getState(): PackingBingoState | null;
package/dist/index.js CHANGED
@@ -7,13 +7,13 @@ import {
7
7
  en,
8
8
  ja,
9
9
  resolveStorageKey
10
- } from "./chunk-Z34GMJW2.js";
10
+ } from "./chunk-BAA2CFUA.js";
11
11
 
12
12
  // src/react.tsx
13
13
  import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
14
14
  import { jsx } from "react/jsx-runtime";
15
15
  var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
16
- const { checked, defaultChecked, className, style, onChange, onBingo, onPerfect, messages, ...config } = props;
16
+ const { checked, defaultChecked, defaultOrder, className, style, onChange, onBingo, onPerfect, messages, fonts: _fonts, ...config } = props;
17
17
  const host = useRef(null);
18
18
  const inst = useRef(null);
19
19
  const callbacks = useRef({ onChange, onBingo, onPerfect });
@@ -25,6 +25,7 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
25
25
  const storageKey = resolveStorageKey({ ...config, controlled });
26
26
  const latestKey = useRef(storageKey);
27
27
  if (controlled) latest.current = checked;
28
+ const latestOrder = useRef(defaultOrder);
28
29
  const messageShape = messages ? Object.fromEntries(Object.entries(messages).map(([k, v]) => [k, typeof v === "function" ? "\u0192" : v])) : null;
29
30
  const key = JSON.stringify({ ...config, messageShape, controlled });
30
31
  useEffect(() => {
@@ -40,6 +41,7 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
40
41
  messages: liveMessages,
41
42
  controlled,
42
43
  defaultChecked: controlled || latestKey.current === storageKey ? latest.current : defaultChecked,
44
+ defaultOrder: latestKey.current === storageKey ? latestOrder.current : defaultOrder,
43
45
  onChange: (next) => {
44
46
  if (!controlled) latest.current = next;
45
47
  callbacks.current.onChange?.(next);
@@ -49,7 +51,9 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
49
51
  });
50
52
  inst.current = instance;
51
53
  return () => {
52
- if (!controlled) latest.current = instance.getState().checked;
54
+ const state = instance.getState();
55
+ if (!controlled) latest.current = state.checked;
56
+ latestOrder.current = state.order;
53
57
  latestKey.current = storageKey;
54
58
  instance.destroy();
55
59
  if (inst.current === instance) inst.current = null;
@@ -65,6 +69,7 @@ var PackingBingo = forwardRef(function PackingBingo2(props, ref) {
65
69
  ref,
66
70
  () => ({
67
71
  reset: () => inst.current?.reset(),
72
+ shuffle: () => inst.current?.shuffle(),
68
73
  setChecked: (next) => inst.current?.setChecked(next, { animate: true }),
69
74
  getState: () => inst.current?.getState() ?? null
70
75
  }),