packing-bingo 0.2.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/README.md CHANGED
@@ -1,24 +1,25 @@
1
1
  # 持ちものビンゴ(Packing Bingo)
2
2
 
3
- **修学旅行の持ち物チェックリストを、4×4のビンゴにするReactコンポーネントです。持ち物をかばんに入れるたびにマスをタップしてスタンプを押し、タテ・ヨコ・ナナメがそろうと、コンボ・ファンファーレ・花火で盛り上がります。進み具合は各自のブラウザに自動で保存されます。**
3
+ **修学旅行の持ち物チェックリストを、4×4(3×3や5×5などにも変えられます)のビンゴにするReactコンポーネントです。持ち物をかばんに入れるたびにマスをタップしてスタンプを押し、タテ・ヨコ・ナナメがそろうと、コンボ・ファンファーレ・花火で盛り上がります。進み具合は各自のブラウザに自動で保存されます。**
4
4
 
5
+ [![Demo on GitHub Pages](https://img.shields.io/badge/demo-GitHub%20Pages-222222?logo=githubpages&logoColor=white)](https://jin-hiroo.github.io/packing-bingo/)
5
6
  [![npm version](https://img.shields.io/npm/v/packing-bingo?logo=npm)](https://www.npmjs.com/package/packing-bingo)
6
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
8
  [![React 18 or later](https://img.shields.io/badge/React-%E2%89%A518-61DAFB?logo=react&logoColor=black)](#概要)
8
9
  [![TypeScript types included](https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white)](#api)
9
10
  [![Standalone single HTML file](https://img.shields.io/badge/standalone-single%20HTML%20file-E34F26?logo=html5&logoColor=white)](#単体のページ)
10
11
 
11
- [クイックスタート](#クイックスタート) · [使い方の例](#使い方の例) · [API](#api) · [組み込むときの注意](#組み込むときの注意) · [Reactなしで使う](#reactなしで使う) · [English](#english) · [変更履歴](CHANGELOG.md)
12
+ [デモサイト](https://jin-hiroo.github.io/packing-bingo/) · [クイックスタート](#クイックスタート) · [デモコード](#デモ) · [使い方の例](#使い方の例) · [API](#api) · [組み込むときの注意](#組み込むときの注意) · [Reactなしで使う](#reactなしで使う) · [English](#english) · [変更履歴](CHANGELOG.md)
12
13
 
13
14
  <p align="center">
14
- <a href="docs/images/bingo-desktop.png"><img src="docs/images/bingo-desktop.png" alt="持ち物が並んだ4×4のビンゴカード。ナナメの列がそろった瞬間で、大きなBINGO!の文字、+100の得点、紙吹雪が出ている。上の欄には8/16、2ライン、2コンボと表示されている。" width="560"></a>
15
+ <a href="docs/images/bingo-desktop.png"><img src="docs/images/bingo-desktop.png" alt="持ち物が並んだ4×4のビンゴカード。一番上の列に続いて、左上からのナナメの列がそろった瞬間で、大きなBINGO!の文字、「2 ライン目!」、+100の得点、紙吹雪が出ている。上の欄には8/16、2ライン、290点、2 COMBOと表示されている。" width="560"></a>
15
16
  </p>
16
17
 
17
18
  _2コンボで2列目がそろったところ。列に線が引かれ、BINGO!と+100が出て、紙吹雪が舞います。_
18
19
 
19
20
  <p>
20
- <a href="docs/images/phone-reach.png"><img src="docs/images/phone-reach.png" alt="スマホの画面。1列そろっていて、次の列を完成させる最後のマスが点線で囲まれ、リーチの札が付いて点滅している。" height="320"></a>
21
- <a href="docs/images/phone-combo.png"><img src="docs/images/phone-combo.png" alt="スマホの画面。3回続けてタップしたところで、+20と+30、2 COMBO!と3 COMBO!の表示、減っていくコンボのゲージ、2つのリーチが見える。" height="320"></a>
21
+ <a href="docs/images/phone-reach.png"><img src="docs/images/phone-reach.png" alt="スマホの画面。一番上の列がそろっていて、ナナメの列を完成させる最後のマス(日焼け止め)が点線で囲まれ、リーチの札が付いて点滅している。下の1行には「リーチ! あと「日焼け止め」でビンゴ」と出ている。" height="320"></a>
22
+ <a href="docs/images/phone-combo.png"><img src="docs/images/phone-combo.png" alt="スマホの画面。3回続けてタップしたところで、+20と+30、2 COMBO!と3 COMBO!の表示、減っていくコンボのゲージ、新しく出た2つのリーチ!が見える。" height="320"></a>
22
23
  <a href="docs/images/phone-perfect.png"><img src="docs/images/phone-perfect.png" alt="スマホの画面。16個すべてにスタンプを押したあと。準備完了のスタンプと、得点3,410、最大コンボ×10、10ラインのパーフェクト画面に、紙吹雪と持ち物の絵文字が降っている。" height="320"></a>
23
24
  </p>
24
25
 
@@ -28,9 +29,11 @@ _スマホでの表示。左から、1列そろって次の列にリーチがか
28
29
 
29
30
  修学旅行の前の晩の荷造りは面倒です。持ちものビンゴは16個の持ち物をビンゴカードに並べるので、持ち物を1つ入れるたびにスタンプが押せて、何個かごとにお祝いがあります。
30
31
 
31
- - **タップするたびに反応があります。** スタンプ、波紋、火花、少しずつ高くなる音。2.5秒以内に続けてタップすると、コンボになります。
32
- - **次の目標がいつも見えます。** あと1つで列がそろうマスは、リーチの札を付けて点滅し、下の1行にその持ち物の名前が出ます。
33
- - **列がそろうたびにお祝いします。** タテ・ヨコ・ナナメの10列のどれかがそろうと、BINGO!の文字と紙吹雪が出ます。16個すべてそろうと、花火とスコアカードのフィナーレになります。
32
+ - **タップするたびに反応があります。** スタンプ、波紋、火花、少しずつ高くなる音、スマホでは振動。2.5秒以内に続けてタップすると、コンボになります。
33
+ - **次の目標がいつも見えます。** あと1つで列がそろうマスは、リーチの札を付けて点滅します。
34
+ - **列がそろうたびにお祝いします。** タテ・ヨコ・ナナメの列(4×4なら10列)のどれかがそろうと、BINGO!の文字と紙吹雪が出ます。すべてそろうと、花火とスコアカードのフィナーレになります。
35
+ - **マスの数を選べます。** 3×3や5×5など、2×2から10×10までの正方形にできます。奇数のときは、真ん中をはじめからスタンプ済みのFREEマスにできます。[マスの数を変える](#マスの数を変える)を見てください。
36
+ - **並びをシャッフルできます。** 🔀ボタンで、持ち物をランダムなマスに並べ替えます。チェックは持ち物といっしょに動くので、同じ持ち物で別の列をねらえます。並べ替えた並びも保存されます。
34
37
  - **得点は、そろえることへのごほうびです。** 付けたり外したりしても増えません。[得点のルール](#得点のルール)を見てください。
35
38
  - **進み具合は自動で保存されます。** 各自のブラウザ(localStorage)に保存されるので、ページを閉じても続きから始められます。サーバーには何も送りません。
36
39
  - **だれでも使えるように配慮しています。** マスは本物のチェックボックスなので、キーボードで操作できます。`prefers-reduced-motion`(動きを減らす設定)に従います。音は合成した音(音声ファイルなし)で、消すこともできます。
@@ -41,11 +44,11 @@ _スマホでの表示。左から、1列そろって次の列にリーチがか
41
44
  | --- | --- |
42
45
  | インストール | `npm install packing-bingo`。ESMとCommonJSの両方、TypeScriptの型付き |
43
46
  | 必要なもの | React 18以上と `react-dom`。ほかに依存するパッケージはありません。CSSの読み込みもProviderも不要です |
44
- | サイズ | 約13 kB(minify・gzip後、CSS込み、Reactは含まず) |
47
+ | サイズ | 約14 kB(minify・gzip後、CSS込み、Reactは含まず) |
45
48
  | サーバー側での表示 | `"use client"` 付きで配布しています。サーバー側では空の `<div>` を出し、ブラウザでカードを作ります |
46
49
  | 見た目 | `<head>` に `<style>` を1つ追加します。クラス名はすべて `pkb-` で始まります。CSS変数で配色を変えられ、置いた場所の幅に合わせて自動で大きさが変わります |
47
- | 通信 | Google Fontsからフォントを2つ読み込みます(`fonts={false}` で止められます) |
48
- | 保存 | localStorageだけ。進み具合と、音のオン・オフの設定 |
50
+ | 通信 | しません。見出しなどのフォントは、使うときにページで読み込みます([フォント](#フォント)) |
51
+ | 保存 | localStorageだけ。進み具合、シャッフルした並び、音のオン・オフの設定 |
49
52
  | ブラウザ | Chrome・Edge 105以上、Safari 16以上、Firefox 121以上。今のところChromiumでしか試していません([詳しく](#対応ブラウザと確認済みの範囲)) |
50
53
 
51
54
  ## クイックスタート
@@ -60,93 +63,109 @@ npm install packing-bingo
60
63
 
61
64
  pnpmなら `pnpm add packing-bingo`、yarnなら `yarn add packing-bingo` です。
62
65
 
63
- ### 2. 部品のファイルを作る
66
+ ### 2. 表示したい場所に置く
64
67
 
65
- `OkinawaPackingBingo.tsx` というファイルを作り、次をそのまま貼り付けます。置き場所はどこでも構いません(例:`components/OkinawaPackingBingo.tsx`)。
68
+ 表示したいページやコンポーネントで読み込み、置きたい場所に `<PackingBingo />` と書きます。一般的な修学旅行の持ち物16個が、4×4のカードに並びます。
66
69
 
67
70
  ```tsx
68
- import { PackingBingo, type BingoItem } from "packing-bingo";
69
-
70
- // 沖縄の修学旅行の持ち物。ちょうど16個、左上から横に4つずつ並びます。
71
- const items: BingoItem[] = [
72
- { emoji: "📖", label: "旅のしおり" },
73
- { emoji: "🪪", label: "生徒手帳" },
74
- { emoji: "🏥", label: "保険証コピー" },
75
- { emoji: "👛", label: "おこづかい" },
76
- { emoji: "👕", label: "着替え" },
77
- { emoji: "🩱", label: "水着" },
78
- { emoji: "🧴", label: "日焼け止め" },
79
- { emoji: "🧢", label: "帽子" },
80
- { emoji: "🕶️", label: "サングラス" },
81
- { emoji: "🩴", label: "ビーチサンダル" },
82
- { emoji: "🪥", label: "洗面用具" },
83
- { emoji: "🛁", label: "タオル" },
84
- { emoji: "💊", label: "常備薬・酔い止め" },
85
- { emoji: "☂️", label: "折りたたみ傘" },
86
- { emoji: "🛍️", label: "ビニール袋" },
87
- { emoji: "📱", label: "スマホ・充電器" },
88
- ];
89
-
90
- export function OkinawaPackingBingo() {
91
- return <PackingBingo items={items} tag="沖縄修学旅行" />;
92
- }
93
- ```
94
-
95
- ### 3. 表示したい場所に置く
96
-
97
- 表示したいページやコンポーネントで読み込み、置きたい場所に `<OkinawaPackingBingo />` と書きます。`import` のパスは、ファイルを置いた場所に合わせてください。
98
-
99
- ```tsx
100
- import { OkinawaPackingBingo } from "./components/OkinawaPackingBingo";
71
+ import { PackingBingo } from "packing-bingo";
101
72
 
102
73
  // JSXの中の、表示したい場所に
103
- <OkinawaPackingBingo />
74
+ <PackingBingo />
104
75
  ```
105
76
 
106
77
  Next.js(App Router)で専用のページを作るなら、`app/packing/page.tsx` を作って次を貼り付けます。`/packing` を開くと表示されます。
107
78
 
108
79
  ```tsx
109
- import { OkinawaPackingBingo } from "@/components/OkinawaPackingBingo";
80
+ import { PackingBingo } from "packing-bingo";
110
81
 
111
82
  export default function PackingPage() {
112
83
  return (
113
84
  <main style={{ padding: 16 }}>
114
- <OkinawaPackingBingo />
85
+ <PackingBingo />
115
86
  </main>
116
87
  );
117
88
  }
118
89
  ```
119
90
 
120
- `@/` は、`create-next-app` で作ったプロジェクトに最初から設定されている、プロジェクトの一番上を指す書き方です。部品のファイルは `components/OkinawaPackingBingo.tsx` に置いてください。`src/` フォルダを使うプロジェクトでは `src/components/OkinawaPackingBingo.tsx` です。`@/` が使えないプロジェクトでは、`../../components/OkinawaPackingBingo` のような相対パスに書き換えてください。
91
+ デモサイトと同じフォントにするなら、ページの `<head>` に次の1行を足します。Next.jsなら `app/layout.tsx` の `<html>` の中に `<head>` を書いてそこへ、Viteなら `index.html` の `<head>` へ入れます。足さなくても動き、そのときは端末のフォントで表示されます([フォント](#フォント))。
92
+
93
+ ```html
94
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap" />
95
+ ```
96
+
97
+ カードは置いた場所の幅いっぱい(最大720 px)に広がり、狭い場所ではスマホ向けの配置に自動で切り替わります。
98
+
99
+ Next.jsのServer Componentのページからでも、そのまま使えます。ただし、`onBingo` などの関数(コールバック)はServer Componentから渡せず、「Event handlers cannot be passed to Client Component props」というエラーでビルドが止まります。コールバックを使うときは、1行目に `"use client";` を書いたファイル(client component)の中に `<PackingBingo>` を置いてください。
121
100
 
122
- カードは置いた場所の幅いっぱい(最大720 px)に広がり、狭い場所ではスマホ向けの配置に自動で切り替わります。持ち物を変えたいときは `items` の中身を書き換えるだけです。`items` を省くと、一般的な修学旅行の持ち物リストになります。
101
+ ### 3. 持ち物を変える
102
+
103
+ `items` に、持ち物を左上から横に1行ずつ並べて渡します。16個なら4×4、9個なら3×3、25個なら5×5になります([マスの数を変える](#マスの数を変える))。次のリストを書き換えて使ってください。
104
+
105
+ ```tsx
106
+ import { PackingBingo, type BingoItem } from "packing-bingo";
107
+
108
+ // 持ち物。16個なら、左上から横に4つずつ並びます。
109
+ // \u200B は「狭い画面ではここで改行してよい」という目印です(画面には出ません)。
110
+ const items: BingoItem[] = [
111
+ { emoji: "👛", label: "財布" },
112
+ { emoji: "📱", label: "スマホ" },
113
+ { emoji: "🔋", label: "モバイル\u200B充電器" },
114
+ { emoji: "📋", label: "健康調査票" },
115
+ { emoji: "🪪", label: "マイナカード\u200Bまたは保険証" },
116
+ { emoji: "📖", label: "しおり" },
117
+ { emoji: "😷", label: "マスク" },
118
+ { emoji: "🤧", label: "ハンカチ" },
119
+ { emoji: "💊", label: "常備薬" },
120
+ { emoji: "🛍️", label: "ビニール袋" },
121
+ { emoji: "🧤", label: "軍手(学校支給)" },
122
+ { emoji: "🔦", label: "ヘッドライト" },
123
+ { emoji: "👕", label: "ジャージ" },
124
+ { emoji: "☔", label: "雨具" },
125
+ { emoji: "🛁", label: "タオル" },
126
+ { emoji: "🧴", label: "日焼け止め" },
127
+ ];
123
128
 
124
- Next.jsのServer Componentのページからでも、そのまま使えます。ただし、`onBingo` などの関数(コールバック)はServer Componentから渡せず、「Event handlers cannot be passed to Client Component props」というエラーでビルドが止まります。コールバックを使うときは、部品のファイルの1行目に `"use client";` を書いてください。
129
+ // JSXの中の、表示したい場所に
130
+ <PackingBingo items={items} />
131
+ ```
125
132
 
126
133
  ### Reactを使っていないページに置く場合
127
134
 
128
- HTMLの好きな場所に、次をそのまま貼り付けます。インストールは要りません。
135
+ HTMLの好きな場所に、次をそのまま貼り付けます。インストールは要りません。1行目はフォントの読み込みで、要らなければ消しても動きます。
129
136
 
130
137
  ```html
138
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap" />
131
139
  <div id="packing-bingo"></div>
132
- <script src="https://cdn.jsdelivr.net/npm/packing-bingo@0.2/dist/packing-bingo.global.js"></script>
140
+ <script src="https://cdn.jsdelivr.net/npm/packing-bingo@0.3/dist/packing-bingo.global.js"></script>
133
141
  <script>
134
142
  PackingBingo.createPackingBingo(document.getElementById("packing-bingo"), {
135
- tag: "沖縄修学旅行",
136
143
  items: [
137
- { emoji: "📖", label: "旅のしおり" }, { emoji: "🪪", label: "生徒手帳" },
138
- { emoji: "🏥", label: "保険証コピー" }, { emoji: "👛", label: "おこづかい" },
139
- { emoji: "👕", label: "着替え" }, { emoji: "🩱", label: "水着" },
140
- { emoji: "🧴", label: "日焼け止め" }, { emoji: "🧢", label: "帽子" },
141
- { emoji: "🕶️", label: "サングラス" }, { emoji: "🩴", label: "ビーチサンダル" },
142
- { emoji: "🪥", label: "洗面用具" }, { emoji: "🛁", label: "タオル" },
143
- { emoji: "💊", label: "常備薬・酔い止め" }, { emoji: "☂️", label: "折りたたみ傘" },
144
- { emoji: "🛍️", label: "ビニール袋" }, { emoji: "📱", label: "スマホ・充電器" },
144
+ { emoji: "👛", label: "財布" }, { emoji: "📱", label: "スマホ" },
145
+ { emoji: "🔋", label: "モバイル\u200B充電器" }, { emoji: "📋", label: "健康調査票" },
146
+ { emoji: "🪪", label: "マイナカード\u200Bまたは保険証" }, { emoji: "📖", label: "しおり" },
147
+ { emoji: "😷", label: "マスク" }, { emoji: "🤧", label: "ハンカチ" },
148
+ { emoji: "💊", label: "常備薬" }, { emoji: "🛍️", label: "ビニール袋" },
149
+ { emoji: "🧤", label: "軍手(学校支給)" }, { emoji: "🔦", label: "ヘッドライト" },
150
+ { emoji: "👕", label: "ジャージ" }, { emoji: "☔", label: "雨具" },
151
+ { emoji: "🛁", label: "タオル" }, { emoji: "🧴", label: "日焼け止め" },
145
152
  ],
146
153
  });
147
154
  </script>
148
155
  ```
149
156
 
157
+ ## デモ
158
+
159
+ - **デモサイト:** https://jin-hiroo.github.io/packing-bingo/
160
+ その場でタップして試せます。演出だけを自動で再生するカードと、組み込み用のコード(このREADMEのクイックスタートと同じもの)も載っています。
161
+ - **デモコード:** [`examples/`](examples/) に、そのまま動かせるプロジェクトがあります。フォルダごとコピーして、自分のサイトの土台にもできます。
162
+
163
+ | フォルダ | 内容 | 動かし方 |
164
+ | --- | --- | --- |
165
+ | [`examples/nextjs`](examples/nextjs) | Next.js(App Router)。クイックスタートそのままのページと、ビンゴの瞬間を受け取るページ | `npm install` → `npm run dev` |
166
+ | [`examples/vite-react`](examples/vite-react) | Vite + React。ふつうのReactアプリのページに置く例 | `npm install` → `npm run dev` |
167
+ | [`examples/html`](examples/html) | HTMLだけ。Reactもインストールも使わない例 | `index.html` をブラウザで開く |
168
+
150
169
  ## 使い方の例
151
170
 
152
171
  ### 持ち物と文言を変える
@@ -155,17 +174,43 @@ HTMLの好きな場所に、次をそのまま貼り付けます。インスト
155
174
  import { PackingBingo, en, type BingoItem } from "packing-bingo";
156
175
 
157
176
  const items: BingoItem[] = [
158
- { emoji: "🩱", label: "Swimsuit" },
159
- { emoji: "🧴", label: "Sunscreen" },
160
- // … ちょうど16個、左上から横に1行ずつ
177
+ { emoji: "👛", label: "Wallet" },
178
+ { emoji: "📱", label: "Phone" },
179
+ // … 4×4なら16個、左上から横に1行ずつ
161
180
  ];
162
181
 
163
- <PackingBingo items={items} messages={en} title="Okinawa Trip" tag="Class 2-B" />;
182
+ <PackingBingo items={items} messages={en} title="School Trip" tag="Class 2-B" />;
164
183
  ```
165
184
 
166
- - `items` はちょうど16個にしてください。それ以外の数だと `[packing-bingo] "items" must have exactly 16 entries` というエラーになります。
185
+ - `items` の数で、カードの大きさが決まります。16個なら4×4、9個なら3×3、25個なら5×5です。正方形にならない数だと、`[packing-bingo] "items" must have a square number of entries` で始まるエラーになります。くわしくは[マスの数を変える](#マスの数を変える)を見てください。
167
186
  - `messages` は日本語の既定値の上に重なるので、`messages={{ reset: "やり直す" }}` のように一部の文言だけを変えられます。`en` は英語の文言一式です。キーの一覧は[文言のキー](#文言のキー)にあります。
168
- - 狭いカードでは、ラベルは `・` の後ろで折り返します。`常備薬・酔い止め` は「常備薬・」と「酔い止め」の2行になります。
187
+ - 狭いカードでは、ラベルは `・` の後ろで折り返します。`常備薬・酔い止め` は「常備薬・」と「酔い止め」の2行になります。`・` を入れたくない長いラベルは、`\u200B`(画面には出ない改行位置の目印)を入れた場所で折り返します。`"モバイル\u200B充電器"` は「モバイル」と「充電器」の2行になります。スマホの幅では、1行に入るのは6文字ほどです。
188
+
189
+ ### マスの数を変える
190
+
191
+ `size` で1辺のマスの数を、`free` で真ん中のFREEマスを決めます。
192
+
193
+ ```tsx
194
+ // 3×3。items を渡さなければ、既定の持ち物の最初の9個を使います
195
+ <PackingBingo size={3} />
196
+
197
+ // 5×5で、真ん中がFREEマス。items は 5×5 − 1 = 24個
198
+ <PackingBingo size={5} free items={items} />
199
+ ```
200
+
201
+ <p>
202
+ <a href="docs/images/phone-5x5-free.png"><img src="docs/images/phone-5x5-free.png" alt="スマホの画面。5×5のカードで、真ん中が赤い丸のFREEマス。左上からのナナメの列がFREEマスを通ってそろい、線が引かれている。真ん中のヨコの列は、残りの「雨具」にリーチの札が付いている。上の欄は9/24、1ライン。" height="320"></a>
203
+ </p>
204
+
205
+ _5×5で真ん中がFREEのカード。FREEマスを通るナナメの列は、残りの4個でそろいます。_
206
+
207
+ - `size` は2〜10です。省略すると、`items` の数から決まります(9個なら3×3、16個なら4×4、25個なら5×5。`free` のときは8個で3×3、24個で5×5)。
208
+ - `items` は `size` × `size` 個、`free` のときはそれより1個少なくします。左上から横に1行ずつ並び、FREEマスは飛ばします。数が合わないと、必要な数を書いたエラーになります。
209
+ - `items` を渡さないときに使う既定の持ち物は16個です。3×3までは最初の何個かを使いますが、5×5以上では `items` が必要です。
210
+ - `free` は `size` が奇数のときだけ使えます。FREEマスは最初からスタンプ済みで、タップできず、持ち物の数にも入りません。FREEマスを通る列(真ん中のタテとヨコ、2本のナナメ)は、残りの `size` − 1 個でそろいます。マスの文字は `messages` の `free` で変えられます(既定は `FREE`)。
211
+ - `checked`・`defaultChecked`・`onChange` の配列は、持ち物の数(FREEマスを除く)と同じ長さです。
212
+ - 文字やすき間は、マスの大きさに合わせて変わります。スマホの幅で読みやすいのは5×5くらいまでです。それより大きいと、ラベルが細かく折り返してマスが縦に伸びます。
213
+ - 持ち物リストから決まる保存のキーは、実際に並ぶ持ち物から決まります。`size={3}` だけのカードと、ふつうの4×4のカードは、別々に保存されます。
169
214
 
170
215
  ### 保存のしかたを変える
171
216
 
@@ -213,14 +258,14 @@ export function PackingCard({ studentId }: { studentId: string }) {
213
258
  }
214
259
  ```
215
260
 
216
- 生徒のタップは通信より速いので、この例では保存を順番に送り、`studentId` が変わったあとに届いた読み込みは捨てています。保存のたびに16個すべてを送るので、最後の保存がいつも最新の状態になります。
261
+ 生徒のタップは通信より速いので、この例では保存を順番に送り、`studentId` が変わったあとに届いた読み込みは捨てています。保存のたびにすべての持ち物の状態を送るので、最後の保存がいつも最新の状態になります。
217
262
 
218
263
  controlledのカードの動き:
219
264
 
220
- - タップしただけでは、カードは変わりません。ユーザーが求めた16個の状態で `onChange` が呼ばれ、`checked` が変わるとカードがそれに合わせます。`checked` を変えなければ、タップは取り消されます。
265
+ - タップしただけでは、カードは変わりません。ユーザーが求めた状態(持ち物の数だけの配列)で `onChange` が呼ばれ、`checked` が変わるとカードがそれに合わせます。`checked` を変えなければ、タップは取り消されます。
221
266
  - `checked` がちょうど1個だけ変わったときは、タップと同じように演出します(スタンプ、音、ビンゴやフィナーレ)。サーバーからデータが届いたときのように一度に何個も変わったときは、演出なしで静かに反映し、同時にそろった列のボーナスも付けません。
222
- - リセットボタンを押すと、16個すべて `false` で `onChange` が呼ばれます。
223
- - `storageKey` は使われません。
267
+ - リセットボタンを押すと、すべて `false` の配列で `onChange` が呼ばれます。
268
+ - `storageKey` は使われません。シャッフルした並びも保存しないので、ページを読み込み直すと元の並びに戻ります。並びも残したいときは、`getState().order` を保存しておき、`defaultOrder` に渡します。
224
269
 
225
270
  ### ビンゴの瞬間を受け取る
226
271
 
@@ -254,7 +299,7 @@ function PackingPage() {
254
299
  }
255
300
  ```
256
301
 
257
- `reset()`・`setChecked()`・`getState()` が使えます。くわしくは [ref](#ref) を見てください。controlledのカードでは、`setChecked()` ではなく `checked` を変えてください。
302
+ `reset()`・`shuffle()`・`setChecked()`・`getState()` が使えます。くわしくは [ref](#ref) を見てください。controlledのカードでは、`setChecked()` ではなく `checked` を変えてください。
258
303
 
259
304
  ### 配色とレイアウト
260
305
 
@@ -281,28 +326,33 @@ function PackingPage() {
281
326
 
282
327
  | prop | 型 | 既定値 | 内容 |
283
328
  | --- | --- | --- | --- |
284
- | `items` | `readonly BingoItem[]`(ちょうど16個) | `DEFAULT_ITEMS`(一般的な修学旅行の持ち物) | マスの中身。左上から横に1行ずつ:`{ emoji, label }` |
329
+ | `items` | `readonly BingoItem[]`(`size` × `size` 個。`free` のときは1個少なく) | `DEFAULT_ITEMS`(一般的な修学旅行の持ち物16個) | マスの中身。左上から横に1行ずつ、FREEマスは飛ばす:`{ emoji, label }` |
330
+ | `size` | `number`(2〜10) | `items` の数から決まる(既定の持ち物なら4) | 1辺のマスの数。[マスの数を変える](#マスの数を変える)を見てください |
331
+ | `free` | `boolean` | `false` | 真ん中をFREEマスにする。`size` が奇数のときだけ |
285
332
  | `messages` | `Partial<Messages>` | `ja` | 画面の文言。日本語の既定値の上に重なります。英語の `en` も入っています |
286
- | `title`・`tag`・`subtitle` | `string` | `messages` の値 | 見出し、その上の小さなラベル、その下の1行 |
333
+ | `title`・`tag`・`subtitle` | `string` | `messages` の値 | 見出し、その上の小さなラベル、その下の1行。`tag`・`subtitle` は既定では空で、表示しません |
287
334
  | `storageKey` | `string \| null` | 持ち物から自動で決まる | 進み具合を保存するlocalStorageのキー。`null` で保存しません。controlledのときは使われません |
288
- | `defaultChecked` | `readonly boolean[]`(16個) | すべて `false` | uncontrolledのカードの最初の状態。保存された進み具合があれば、そちらが優先です |
289
- | `checked` | `readonly boolean[]`(16個) | なし | 指定するとcontrolledになります。[自分のサーバーに保存する](#自分のサーバーに保存する)を見てください |
335
+ | `defaultChecked` | `readonly boolean[]`(持ち物と同じ数) | すべて `false` | uncontrolledのカードの最初の状態。保存された進み具合があれば、そちらが優先です |
336
+ | `checked` | `readonly boolean[]`(持ち物と同じ数) | なし | 指定するとcontrolledになります。[自分のサーバーに保存する](#自分のサーバーに保存する)を見てください |
290
337
  | `onChange` | `(checked: boolean[]) => void` | なし | ユーザーがタップかリセットをした。controlledのときは、求められた状態 |
291
338
  | `onBingo` | `(event: BingoEvent) => void` | なし | タップで1列以上そろった |
292
339
  | `onPerfect` | `(event: PerfectEvent) => void` | なし | タップでカード全体がそろった |
293
340
  | `sound` | `boolean` | `true` | 音の最初の設定。ユーザーが🔊ボタンで選ぶまで使われます |
294
- | `fonts` | `boolean` | `true` | Dela Gothic OneとM PLUS Rounded 1cをGoogle Fontsから読み込む |
295
- | `demo` | `boolean` | `false` | デモ用のボタンを出します。保存された進み具合も `defaultChecked` もなければ、6マス埋まった状態から始めます。`storageKey` を指定しない限り保存しません |
341
+ | `shuffle` | `boolean` | `true` | 🔀シャッフルボタンを出します。`false` でボタンを消しても、`ref.shuffle()` は使えます |
342
+ | `defaultOrder` | `readonly number[]`(持ち物と同じ数) | 元の並び | 最初の並び。`getState().order` と同じ形です。シャッフルした並びが保存されていれば、そちらが優先です。どの持ち物の番号もちょうど1回ずつ入っていないときは使いません |
343
+ | `vibrate` | `boolean` | `true` | スマホで振動します。[音と振動](#音と振動)を見てください |
344
+ | `fonts` | `boolean` | なし | 非推奨。0.4.0からは何もしません。フォントはページで読み込みます([フォント](#フォント)) |
345
+ | `demo` | `boolean` | `false` | デモ用のボタンを出します。保存された進み具合も `defaultChecked` もなければ、一番上の列と、左上からのナナメの途中までが埋まった状態(4×4なら6マス)から始めます。`storageKey` を指定しない限り保存しません |
296
346
  | `className`・`style` | | なし | 外側の `<div>` に付きます |
297
347
  | `ref` | `Ref<PackingBingoHandle>` | なし | [ref](#ref) を見てください |
298
348
 
299
- propsは中身で比べるので、配列やオブジェクトをその場で書いても構いません。`items`・`title`・`tag`・`subtitle`・`storageKey`・`sound`・`fonts`・`demo`、または `messages` の文字列が変わると、カードを作り直します。保存先のキーが変わらなければ、チェックはそのまま残ります。キーが変わったとき(`items` を変えて自動のキーが変わったときも含む)は、新しいキーに保存された進み具合を読み込みます。得点は[得点のルール](#得点のルール)のとおり計算し直します。`messages` の関数はpropsやstateを使っても構いません。`messages` が変わると、作り直さずに下の1行だけを描き直します。
349
+ propsは中身で比べるので、配列やオブジェクトをその場で書いても構いません。`items`・`size`・`free`・`title`・`tag`・`subtitle`・`storageKey`・`sound`・`shuffle`・`vibrate`・`demo`、または `messages` の文字列が変わると、カードを作り直します。保存先のキーが変わらなければ、チェックとシャッフルした並びはそのまま残ります。キーが変わったとき(`items` を変えて自動のキーが変わったときも含む)は、新しいキーに保存された進み具合を読み込みます。得点は[得点のルール](#得点のルール)のとおり計算し直します。`messages` の関数はpropsやstateを使っても構いません。`messages` が変わると、作り直さずに下の1行だけを描き直します。
300
350
 
301
351
  ### イベント
302
352
 
303
353
  ```ts
304
354
  interface BingoEvent {
305
- newLines: number[]; // このタップでそろった列(LINESの番号)
355
+ newLines: number[]; // このタップでそろった列(bingoLines(size, free) の番号。4×4ならLINESと同じ。シャッフルしたあとも、マスの位置の番号)
306
356
  totalLines: number; // これまでにそろった列の数
307
357
  score: number; // このタップのあとの得点
308
358
  }
@@ -317,8 +367,10 @@ interface PerfectEvent {
317
367
 
318
368
  ```ts
319
369
  interface PackingBingoHandle {
320
- /** リセットボタンと同じ。カードと得点を消して onChange を呼ぶ。controlled のカードでは onChange を呼ぶだけ。 */
370
+ /** リセットボタンと同じ。カードと得点を消し、持ち物を元のマスに戻して onChange を呼ぶ。controlled のカードでは、並びを戻して onChange を呼ぶだけ。 */
321
371
  reset(): void;
372
+ /** 🔀ボタンと同じ。持ち物をランダムなマスに並べ替える。チェックは持ち物といっしょに動き、onChange は呼ばない。 */
373
+ shuffle(): void;
322
374
  /** 1個だけの変化はタップと同じように演出し、それより大きな変化は静かに反映する。onChange は呼ばない。 */
323
375
  setChecked(checked: readonly boolean[]): void;
324
376
  /** 表示前とサーバー側では null。 */
@@ -326,63 +378,84 @@ interface PackingBingoHandle {
326
378
  }
327
379
 
328
380
  interface PackingBingoState {
329
- checked: boolean[]; // 16個、左上から横に1行ずつ
330
- lines: number[]; // そろった列(LINESの番号)
381
+ checked: boolean[]; // 持ち物ごと、左上から横に1行ずつ(FREEマスは含まない)
382
+ lines: number[]; // そろった列(bingoLines(size, free) の番号)
331
383
  score: number;
332
384
  maxCombo: number;
333
- complete: boolean; // 16個すべてにチェックが付いている
385
+ complete: boolean; // すべての持ち物にチェックが付いている
386
+ size: number; // 1辺のマスの数
387
+ free: boolean; // 真ん中がFREEマスか
388
+ order: number[]; // マスごとの持ち物の番号(左上から横に1行ずつ、FREEマスは飛ばす)。シャッフルするまでは [0, 1, 2, …]
334
389
  }
335
390
  ```
336
391
 
337
- `LINES` もexportしています。0〜3がヨコの列、4〜7がタテの列、8が左上からのナナメ、9が右上からのナナメです。
392
+ `LINES` もexportしています。4×4のカードの列で、0〜3がヨコの列、4〜7がタテの列、8が左上からのナナメ、9が右上からのナナメです。ほかの大きさの列は `bingoLines(size, free)` で取れます。並び方は同じで、ヨコの列、タテの列、左上からのナナメ、右上からのナナメの順です。中身は `checked` と同じ持ち物の番号なので、`free` のときは真ん中を通る列だけ `size` − 1 個になります。これは元の並びのときの番号です。シャッフルしたカードでは、列 `k` の持ち物は `bingoLines(size, free)[k].map((s) => order[s])` です(`order` は `getState().order`)。
338
393
 
339
394
  ### 得点のルール
340
395
 
341
396
  - 初めてチェックした持ち物は、10点 × そのときのコンボ数。コンボは、2.5秒以内に続けたタップの数です。
342
- - 1回のタップでn列そろうと、100点 × n²。同じ列で得点するのは1回だけです。
397
+ - 1回のタップでn列そろうと、100点 × n²。同じ持ち物の組み合わせの列で得点するのは1回だけです(シャッフルしたあとも同じです)。
343
398
  - 初めてカード全体をそろえると、1000点を足します。
344
399
  - チェックを外しても得点は減らず、付け直しても増えません。
400
+ - シャッフルしても得点は変わりません。並べ替えたことでそろった列は、そろった列として数えますが、得点もお祝いもありません。
401
+ - シャッフル時に一度でもチェックしたことのある持ち物だけでできる列は、新しい組み合わせでも得点しません。シャッフル前にチェックを外しても同じです。
345
402
  - 静かに反映したあとや作り直したあと(保存された進み具合を読み込んだときなど)は、チェックした持ち物1個につき10点、そろった列1つにつき100点で計算し直し、最大コンボは0からになります。
346
403
 
347
404
  ### 文言のキー
348
405
 
349
406
  `messages` には、次のキーを好きなだけ指定できます。日本語と英語の一式は、[`src/messages.ts`](src/messages.ts) の `ja` と `en` です。
350
407
 
351
- - **文字列:** `title`、`tag`、`subtitle`、`packed`、`bingo`、`score`、`reset`、`soundOn`、`soundOff`、`stamp`、`reachBadge`、`reachPop`、`bannerSingle`、`bannerDouble`、`bannerTriple`、`statusDone`、`perfectTag`、`perfectStamp`、`perfectTitle`、`perfectBody`、`statScore`、`statCombo`、`statLines`、`close`、`demoLabel`、`demoBingo`、`demoPerfect`、`demoRestore`。`perfectStamp` と `perfectBody` では、`\n` で改行します。
408
+ - **文字列:** `title`、`tag`、`subtitle`、`packed`、`bingo`、`score`、`reset`、`shuffle`、`soundOn`、`soundOff`、`stamp`、`free`、`reachBadge`、`reachPop`、`bannerSingle`、`bannerDouble`、`bannerTriple`、`statusDone`、`perfectTag`、`perfectStamp`、`perfectTitle`、`perfectBody`、`statScore`、`statCombo`、`statLines`、`close`、`demoLabel`、`demoBingo`、`demoPerfect`、`demoRestore`。`perfectStamp` と `perfectBody` では、`\n` で改行します。
352
409
  - **関数(どれも文字列を返します):** `combo(n)`、`comboPop(n)`、`bannerNth(totalLines)`、`bannerMulti(newLines)`、`statusStart(left)`、`statusLines(lines, left)`、`statusReach(labels)`、`perfectBonus(points)`。
410
+ - `perfectBody` は、文字列のほかに、持ち物の数を受け取る関数 `perfectBody(total)` にもできます。`ja` と `en` では関数で、「16こ ぜんぶそろった!」の数がカードに合わせて変わります。
411
+ - `subtitle`(見出しの下の1行)と `statusStart(left)`(1列もそろう前の下の1行)は、`ja` と `en` のどちらでも空なので、表示しません。出したいときは `subtitle="タップでスタンプ。タテ・ヨコ・ナナメでビンゴ!"` や `` messages={{ statusStart: (left) => `のこり ${left} こ` }} `` のように指定します。
353
412
 
354
413
  ### export
355
414
 
356
- - `packing-bingo`:`PackingBingo`、`createPackingBingo`、`LINES`、`DEFAULT_ITEMS`、`ja`、`en`。型は `PackingBingoProps`、`PackingBingoHandle`、`PackingBingoState`、`BingoItem`、`BingoEvent`、`PerfectEvent`、`Messages`、`PackingBingoOptions`、`PackingBingoInstance`。
415
+ - `packing-bingo`:`PackingBingo`、`createPackingBingo`、`LINES`、`bingoLines`、`DEFAULT_ITEMS`、`ja`、`en`。型は `PackingBingoProps`、`PackingBingoHandle`、`PackingBingoState`、`BingoItem`、`BingoEvent`、`PerfectEvent`、`Messages`、`PackingBingoOptions`、`PackingBingoInstance`。
357
416
  - `packing-bingo/core`:Reactのコンポーネントとその型(`PackingBingoProps`、`PackingBingoHandle`)を除いた残り。Reactを読み込みません。
358
417
 
359
418
  ## 組み込むときの注意
360
419
 
361
420
  ### ページに追加されるもの
362
421
 
363
- 最初に表示したとき、`<head>` に `<style id="pkb-styles">` を追加します。これはすべてのカードで共有し、カードを外したあとも残ります。`fonts={false}` でなければ、Google Fonts用の `<link id="pkb-fonts">` も追加します。CSSはすべて `.pkb` の中だけに効き、クラス名とアニメーション名はすべて `pkb-` で始まるので、ページのほかの部分には影響しません。
422
+ 最初に表示したとき、`<head>` に `<style id="pkb-styles">` を追加します。これはすべてのカードで共有し、カードを外したあとも残ります。ほかには何も追加せず、外部にも何も取りに行きません。CSSはすべて `.pkb` の中だけに効き、クラス名とアニメーション名はすべて `pkb-` で始まるので、ページのほかの部分には影響しません。
364
423
 
365
424
  カードが描くもの(紙吹雪、BINGO!の文字、フィナーレの画面)は、すべてカード自身の枠の中に収まります。`position: fixed` を使ったり `<body>` に要素を足したりはしません。カードは独立した重なり順(stacking context)を持つので、上部に固定したヘッダーの上に描かれることもありません。
366
425
 
367
426
  ### CSP(Content Security Policy)
368
427
 
369
- 追加する `<style>` と一部の `style` 属性のために、`style-src 'unsafe-inline'` が必要です。フォントを読み込む場合は、`style-src` に `https://fonts.googleapis.com`、`font-src` に `https://fonts.gstatic.com` も許可してください。
428
+ 追加する `<style>` と一部の `style` 属性のために、`style-src 'unsafe-inline'` が必要です。[フォント](#フォント)をGoogle Fontsから読み込む場合は、`style-src` に `https://fonts.googleapis.com`、`font-src` に `https://fonts.gstatic.com` も許可してください。
370
429
 
371
430
  ### フォント
372
431
 
373
- Dela Gothic OneとM PLUS Rounded 1cを使います。`fonts={false}` にするとGoogleには何も取りに行きません。サイトで同じ名前のフォントを読み込んでいれば(自分のサーバーに置いている場合など)それを使い、なければ `--pkb-font-fallback` のフォントを使います。
432
+ 見出しなどにDela Gothic One、本文にM PLUS Rounded 1cを使います。カードはフォントを読み込まないので、使うときはページで読み込みます。Google Fontsからなら、`<head>` に次の1行です。
433
+
434
+ ```html
435
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=M+PLUS+Rounded+1c:wght@500;800&display=swap" />
436
+ ```
437
+
438
+ 自分のサーバーに置いたフォントでも、同じ名前(`font-family`)で読み込めば使われます。読み込まなければ、本文はヒラギノ丸ゴシック(Appleの端末)、見出しなどとそれ以外の端末の本文は `--pkb-font-fallback` のフォント(既定は `sans-serif`)になります。
439
+
440
+ 0.3.0までは、カードがGoogle Fontsの `<link>` を自動で追加していました。0.4.0からは追加せず、`fonts` も何もしません。
374
441
 
375
442
  ### 音と振動
376
443
 
377
- 音はWeb Audio APIで合成しています。音を鳴らす仕組みは、ユーザーがページを操作したあとにだけ作るので、ブラウザの自動再生の警告は出ません。🔊ボタンの選択は、localStorageの `packing-bingo-muted` に保存されます。そのあとに作られるカードは、ページを読み込み直したあとも含めて、この設定で始まります。すでに表示中のほかのカードは、それぞれの設定のままです。タップやビンゴのときは、Vibration APIがある端末で振動します。実際にはAndroidのブラウザです。
444
+ 音はWeb Audio APIで合成しています。音を鳴らす仕組みは、ユーザーがページを操作したあとにだけ作るので、ブラウザの自動再生の警告は出ません。🔊ボタンの選択は、localStorageの `packing-bingo-muted` に保存されます。そのあとに作られるカードは、ページを読み込み直したあとも含めて、この設定で始まります。すでに表示中のほかのカードは、それぞれの設定のままです。
445
+
446
+ 振動は端末によって違います。`vibrate={false}` で、どちらも止められます。
447
+
448
+ - **Android**(Vibration APIがあるブラウザ):タップで短く、外したときはもっと短く振動します。ビンゴ、パーフェクト、シャッフルのときは、それぞれのパターンで振動します。
449
+ - **iPhone**(iOS 18以上のSafari):Vibration APIがないので、マスのチェックボックスに `switch` 属性を付けています。指でタップしてチェックが切り替わると、iPhoneの軽い触覚フィードバックが出ます。出るのはタップの瞬間だけで、ビンゴなどの演出に合わせた振動はありません。`role="checkbox"` も付けているので、読み上げは今までどおりチェックボックスです。ほかのブラウザは `switch` 属性を無視します。
450
+ - 端末の設定で振動や触覚を切っていると、振動しません。
378
451
 
379
452
  ### 保存されるデータ
380
453
 
381
- チェックは、16個の真偽値のJSON配列としてlocalStorageに保存されます。キーは `storageKey`、指定しなければ `packing-bingo:` で始まる、持ち物リストから決まるキーです。得点は保存しません。保存された進み具合は `defaultChecked` より優先されます。プライベートブラウズなどで保存できない環境では、保存せずにそのまま動きます。
454
+ チェックは、持ち物の数(4×4なら16個。FREEマスは含みません)だけの真偽値のJSON配列として、localStorageに保存されます。キーは `storageKey`、指定しなければ `packing-bingo:` で始まる、持ち物リストから決まるキーです。得点は保存しません。保存された進み具合は `defaultChecked` より優先されます。シャッフルした並びは、`packing-bingo-order:` のあとにそのキーを付けたキー(たとえば `packing-bingo-order:okinawa-2026`)に、マスごとの持ち物の番号のJSON配列として保存します。元の並びに戻したときも保存するので、`defaultOrder` を指定していても、次に開くと元の並びのままです。プライベートブラウズなどで保存できない環境では、保存せずにそのまま動きます。
382
455
 
383
456
  ### アクセシビリティ
384
457
 
385
- マスは `<label>` の中の本物のチェックボックスなので、TabキーとSpaceキーで操作でき、持ち物の名前がそのまま読み上げられます。絵文字とスタンプは、読み上げの対象から外しています。下の1行は、変化を控えめに読み上げる領域(polite live region)です。`prefers-reduced-motion` のときは、揺れ、弾み、紙吹雪を止めます。スクリーンリーダーでの確認は、まだしていません。
458
+ マスは `<label>` の中の本物のチェックボックスなので、TabキーとSpaceキーで操作でき、持ち物の名前がそのまま読み上げられます。絵文字とスタンプは、読み上げの対象から外しています。下の1行は、変化を控えめに読み上げる領域(polite live region)です。シャッフルするとマスのHTMLの順番も入れ替わるので、Tabキーで進む順番は見た目の並びと同じです。フォーカスしていたマスは、動いたあともフォーカスしたままです。`prefers-reduced-motion` のときは、揺れ、弾み、紙吹雪を止めます。スクリーンリーダーでの確認は、まだしていません。
386
459
 
387
460
  ### 対応ブラウザと確認済みの範囲
388
461
 
@@ -396,8 +469,10 @@ CSSのコンテナクエリ(Chrome・Edge 105以上、Safari 16以上、Firefo
396
469
  - npmとpnpmでのnpmからのインストール
397
470
  - Chromiumで、パソコンとスマホの大きさでの自動タップ。演出の途中でのリセットやコールバックも含む
398
471
  - 自動保存。持ち物リストごとに別々に保存されること、`storageKey={null}` で保存しないこと、持ち物を変えると新しく始まり、戻すと前の進み具合が戻ること
472
+ - Chromiumでのシャッフル。チェックと得点が変わらないこと、何度シャッフルしても付け直しても得点が増えないこと、並びの保存と読み込み、リセットで元の並びに戻ること、FREEマスが動かないこと、Tabキーの順番とフォーカス
473
+ - Vibration APIを呼ぶこと(呼び出しを記録して確認。実際の振動は未確認)
399
474
 
400
- これらの自動テストは、まだこのリポジトリには入っていません。Safari、Firefox、実際のスマホでは試しておらず、実機での音も確かめていません。絵文字は端末の絵文字フォントで表示されるので、端末ごとに見た目が違います。
475
+ これらの自動テストは、まだこのリポジトリには入っていません。Safari、Firefox、実際のスマホでは試しておらず、実機での音と振動も確かめていません。iPhoneの触覚フィードバックは、`switch` 属性のチェックボックスがもともと持っている動きを使ったもので、iOSのバージョンによっては出ないことがあります。iOS 26.5では、スクリプトから切り替えたときの触覚が出なくなったと報告されています。このカードが使うのは指でのタップだけです。絵文字は端末の絵文字フォントで表示されるので、端末ごとに見た目が違います。
401
476
 
402
477
  ## Reactなしで使う
403
478
 
@@ -407,10 +482,10 @@ CSSのコンテナクエリ(Chrome・Edge 105以上、Safari 16以上、Firefo
407
482
  import { createPackingBingo } from "packing-bingo/core";
408
483
 
409
484
  const card = createPackingBingo(document.getElementById("bingo")!, {
410
- tag: "沖縄修学旅行",
485
+ tag: "2年B組",
411
486
  onBingo: (e) => console.log(e),
412
487
  });
413
- // card.reset()、card.getState()、card.setChecked(next, { animate: true })、card.destroy()
488
+ // card.reset()、card.shuffle()、card.getState()、card.setChecked(next, { animate: true })、card.destroy()
414
489
  ```
415
490
 
416
491
  オプションは、Reactのpropsから `checked`・`className`・`style`・`ref` を除いたものです(型は `PackingBingoOptions`)。controlledにするときは `controlled: true` を渡し、`setChecked()` で状態を変えます。`setChecked()` は、`{ animate: true }` を付けないと、いつも静かに反映します。
@@ -427,7 +502,7 @@ cd packing-bingo
427
502
  open index.html # macOSの場合。ほかのOSでは、ファイルをブラウザで開いてください
428
503
  ```
429
504
 
430
- URLの末尾に `?demo` を付けると、途中まで埋まった状態から始まり、デモ用のボタンが出ます。「▶ ビンゴを見る」でビンゴを、「▶ コンボ → パーフェクト」でコンボからパーフェクトまでを再生します。GitHub Pagesで公開するときは、**Settings → Pages → Deploy from a branch → `main` / root** を選びます。
505
+ URLの末尾に `?demo` を付けると、途中まで埋まった状態から始まり、デモ用のボタンが出ます。「▶ ビンゴを見る」でビンゴを、「▶ コンボ → パーフェクト」でコンボからパーフェクトまでを再生します。デモサイトの [standalone.html](https://jin-hiroo.github.io/packing-bingo/standalone.html) でも同じページを公開しています。
431
506
 
432
507
  ページの中身を変えるときは、最後の `<script>` にある `PackingBingo.createPackingBingo(…)` のオプションを書き換えます。どのビルドでも使われる既定値を変えるときは、`src/items.ts` か `src/messages.ts` を編集して `npm run build` を実行します。
433
508
 
@@ -441,9 +516,11 @@ npm run typecheck
441
516
 
442
517
  `index.html` は `scripts/build-standalone.mjs` が `src/` から作るので、直接編集しないでください。
443
518
 
519
+ デモサイトは `site/index.html` です。`node scripts/build-site.mjs` を実行すると、`_site/` にデモサイトができます。そのとき、組み込み用のコードはこのREADMEのクイックスタートから差し込まれるので、READMEとデモサイトのコードはいつも同じになります。`main` に変更が入ると、GitHub Actions(`.github/workflows/pages.yml`)がビルドしてGitHub Pagesに公開し直します。
520
+
444
521
  ## English
445
522
 
446
- Packing Bingo is a React component that turns a school-trip packing checklist into a 4×4 bingo card, with combos, fanfares and fireworks. Progress is saved in each student's browser automatically. It has no runtime dependencies and is safe to render on the server (it ships with `"use client"`).
523
+ Packing Bingo is a React component that turns a school-trip packing checklist into a bingo card, with combos, fanfares and fireworks. Cards are 4×4 by default; `size` makes them anything from 2×2 to 10×10, and `free` puts a FREE square in the center of an odd-sized card. Progress is saved in each student's browser automatically. A 🔀 button moves the items to random squares (checks move with their items, and the arrangement is saved too; `shuffle={false}` hides it). Taps buzz on phones: through the Vibration API on Android, and with the system tap haptic of a `switch` checkbox on iPhone (iOS 18+); `vibrate={false}` turns both off. It has no runtime dependencies, makes no network requests, and is safe to render on the server (it ships with `"use client"`). It does not load its web fonts either: add the Google Fonts `<link>` from the quick start to the page's `<head>` to get them.
447
524
 
448
525
  ```sh
449
526
  npm install packing-bingo
@@ -455,10 +532,10 @@ import { PackingBingo, en } from "packing-bingo";
455
532
  <PackingBingo messages={en} />;
456
533
  ```
457
534
 
458
- The default UI text is Japanese; `en` is a complete English set. Pass exactly 16 `items` (`{ emoji, label }`) to use your own list. The sections above cover the props, events and integration notes; the code snippets and type names can be read as they are.
535
+ Try it on the [demo site](https://jin-hiroo.github.io/packing-bingo/); runnable example projects (Next.js, Vite + React, plain HTML) are in [`examples/`](examples/). The default UI text is Japanese; `en` is a complete English set. Pass your own `items` (`{ emoji, label }`, row by row): 16 for 4×4, size² in general, or size² − 1 with `free` (the FREE square is skipped, also in `checked`). Without `size`, the number of items sets it. `bingoLines(size, free)` gives the winning lines as item indices. The sections above cover the props, events and integration notes; the code snippets and type names can be read as they are.
459
536
 
460
537
  ## ライセンス
461
538
 
462
539
  [MIT](LICENSE) © 2026 Jin-hiroo.
463
540
 
464
- Dela Gothic OneとM PLUS Rounded 1cのフォントはGoogle Fontsから読み込んでおり、SIL Open Font Licenseで提供されています。このリポジトリには含まれていません。
541
+ Dela Gothic OneとM PLUS Rounded 1cは、SIL Open Font Licenseで提供されているフォントです。このパッケージにもリポジトリにも含まれていません。デモサイトと単体のページは、Google Fontsから読み込んでいます。